diff --git a/client/src/assets/icons/Cup.js b/client/src/assets/icons/Cup.js
new file mode 100644
index 00000000000..8e0914ac568
--- /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
+
+
+ );
+}
+
+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 1afc60d0596..c1f03f70cd0 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 400063ed93b..d84a0676353 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 16573021272..a8d3ef06a60 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 338808dde39..b67bf24b37e 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 92cbeb35a30..48de051c86f 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 0e4d415ee13..5431b08a5ee 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
);
});
});