feat: activate progress based donation modal (#37882)

This commit is contained in:
Ahmad Abdolsaheb
2019-12-09 17:30:24 +01:00
committed by mrugesh
parent f1ddec3f9b
commit 3f075f91d8
7 changed files with 222 additions and 83 deletions

View File

@ -0,0 +1,59 @@
/* eslint-disable max-len */
import React, { Fragment } from 'react';
const propTypes = {};
function Cup(props) {
return (
<Fragment>
<span className='sr-only'>Gold Cup</span>
<svg
height={200}
version='1.1'
viewBox='0 0 172 200'
width={172}
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<title>Gold Cup</title>
<g fill='none' fillRule='evenodd'>
<g transform='translate(-14)'>
<g transform='translate(20)'>
<rect
fill='#ffbf00'
height={22}
id='b'
transform='translate(80 166.5) rotate(-90) translate(-80 -166.5)'
width={69}
x='45.5'
y='155.5'
/>
<rect fill='#ffbf00' height={11} width={85} x={39} y={190} />
<rect fill='#ffbf00' height={12} width={62} x={51} y={179} />
<rect fill='#ffbf00' height={12} width={112} x={24} />
<rect fill='#ffbf00' height={98} width={95} x={33} y={11} />
<path
d='m9.929 33.949c18.201-5.245 30.238-3.4924 36.112 5.258 8.8109 13.126-11.162 53.58-34.056 67.467-15.262 9.2576-15.948-14.984-2.056-72.724z'
id='a'
stroke='#ffbf00'
strokeWidth={11}
transform='translate(24.084 69.796) scale(-1 1) translate(-24.084 -69.796)'
/>
<path
d='m121.93 33.949c18.201-5.245 30.238-3.4924 36.112 5.258 8.8109 13.126-11.162 53.58-34.056 67.467-15.262 9.2576-15.948-14.984-2.056-72.724z'
stroke='#ffbf00'
strokeWidth={11}
/>
<circle cx='80.5' cy='106.5' fill='#ffbf00' r='47.5' />
</g>
</g>
</g>
</svg>
</Fragment>
);
}
Cup.displayName = 'Cup';
Cup.propTypes = propTypes;
export default Cup;

View File

@ -176,7 +176,7 @@ li.disabled > a {
} }
} }
.heart-icon-container { .donation-icon-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -184,15 +184,15 @@ li.disabled > a {
margin: 40px; margin: 40px;
} }
.heart-icon { .donation-icon {
width: 150px; width: 150px;
height: auto; height: auto;
transform: scale(1.5); transform: scale(1.5);
opacity: 0; 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% { 33% {
transform: scale(1.2); transform: scale(1.2);
} }
@ -206,10 +206,9 @@ li.disabled > a {
} }
.donation-modal p { .donation-modal p {
margin: 0;
text-align: center; text-align: center;
font-weight: 700; font-weight: 700;
font-size: 1.2rem; font-size: 1.1rem;
} }
.donation-modal .modal-title { .donation-modal .modal-title {
@ -219,7 +218,7 @@ li.disabled > a {
} }
@media screen and (max-width: 991px) { @media screen and (max-width: 991px) {
.heart-icon-container { .donation-icon-container {
margin: 30px; margin: 30px;
} }
.donation-modal p { .donation-modal p {

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react'; /* eslint-disable max-len */
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -7,11 +8,14 @@ import { Modal, Button } from '@freecodecamp/react-bootstrap';
import { Link } from '../../../components/helpers'; import { Link } from '../../../components/helpers';
import { blockNameify } from '../../../../utils/blockNameify'; import { blockNameify } from '../../../../utils/blockNameify';
import Heart from '../../../assets/icons/Heart'; import Heart from '../../../assets/icons/Heart';
import Cup from '../../../assets/icons/Cup';
import ga from '../../../analytics'; import ga from '../../../analytics';
import { import {
closeDonationModal, closeDonationModal,
isDonationModalOpenSelector isDonationModalOpenSelector,
isBlockDonationModalSelector,
activeDonationsSelector
} from '../../../redux'; } from '../../../redux';
import { challengeMetaSelector } from '../../../templates/Challenges/redux'; import { challengeMetaSelector } from '../../../templates/Challenges/redux';
@ -21,9 +25,13 @@ import '../Donation.css';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isDonationModalOpenSelector, isDonationModalOpenSelector,
challengeMetaSelector, challengeMetaSelector,
(show, { block }) => ({ isBlockDonationModalSelector,
activeDonationsSelector,
(show, { block }, isBlockDonation, activeDonors) => ({
show, show,
block block,
isBlockDonation,
activeDonors
}) })
); );
@ -36,17 +44,52 @@ const mapDispatchToProps = dispatch =>
); );
const propTypes = { const propTypes = {
activeDonors: PropTypes.number,
block: PropTypes.string, block: PropTypes.string,
closeDonationModal: PropTypes.func.isRequired, closeDonationModal: PropTypes.func.isRequired,
isBlockDonation: PropTypes.bool,
show: PropTypes.bool show: PropTypes.bool
}; };
class DonateModal extends Component { function DonateModal({
render() { show,
const { show, block } = this.props; block,
activeDonors,
isBlockDonation,
closeDonationModal
}) {
if (show) { if (show) {
ga.modalview('/donation-modal'); ga.modalview('/donation-modal');
} }
const blockDonationText = (
<div className='block-modal-text'>
<div className='donation-icon-container'>
<Cup className='donation-icon' />
</div>
<p className='text-center'>
Nicely done. You just completed {blockNameify(block)}.
</p>
<p className='text-center'>
Help us create even more learning resources like this.
</p>
</div>
);
const progressDonationText = (
<div className='text-center progress-modal-text'>
<div className='donation-icon-container'>
<Heart className='donation-icon' />
</div>
<p>
freeCodeCamp.org is a tiny nonprofit that's helping millions of people
learn to code for free.
</p>
<p>
Join <strong>{activeDonors}</strong> supporters.
</p>
<p>Your donation will help keep tech education free and open.</p>
</div>
);
return ( return (
<Modal bsSize='lg' className='donation-modal' show={show}> <Modal bsSize='lg' className='donation-modal' show={show}>
@ -56,20 +99,12 @@ class DonateModal extends Component {
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<p className='text-center'> {isBlockDonation ? blockDonationText : progressDonationText}
Nicely done. You just completed {blockNameify(block)}.
</p>
<div className='heart-icon-container'>
<Heart className='heart-icon' />
</div>
<p className='text-center'>
Help us create even more learning resources like this.
</p>
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Link <Link
className='btn-invert btn btn-lg btn-primary btn-block btn-cta' className='btn-invert btn btn-lg btn-primary btn-block btn-cta'
onClick={this.props.closeDonationModal} onClick={closeDonationModal}
to={`/donate`} to={`/donate`}
> >
Support our nonprofit Support our nonprofit
@ -79,14 +114,13 @@ class DonateModal extends Component {
bsSize='lg' bsSize='lg'
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
onClick={this.props.closeDonationModal} onClick={closeDonationModal}
> >
Ask me later Ask me later
</Button> </Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
); );
}
} }
DonateModal.displayName = 'DonateModal'; DonateModal.displayName = 'DonateModal';

View File

@ -2,16 +2,23 @@ import { put, select, takeEvery, delay } from 'redux-saga/effects';
import { import {
openDonationModal, openDonationModal,
preventDonationRequests, preventBlockDonationRequests,
shouldRequestDonationSelector shouldRequestDonationSelector,
preventProgressDonationRequests,
canRequestBlockDonationSelector
} from './'; } from './';
function* showDonateModalSaga() { function* showDonateModalSaga() {
let shouldRequestDonation = yield select(shouldRequestDonationSelector); let shouldRequestDonation = yield select(shouldRequestDonationSelector);
if (shouldRequestDonation) { if (shouldRequestDonation) {
yield delay(200); yield delay(200);
yield put(openDonationModal()); const isBlockDonation = yield select(canRequestBlockDonationSelector);
yield put(preventDonationRequests()); yield put(openDonationModal(isBlockDonation));
if (isBlockDonation) {
yield put(preventBlockDonationRequests());
} else {
yield put(preventProgressDonationRequests());
}
} }
} }

View File

@ -32,7 +32,8 @@ export const defaultFetchState = {
const initialState = { const initialState = {
appUsername: '', appUsername: '',
canRequestDonation: false, canRequestBlockDonation: false,
canRequestProgressDonation: true,
completionCount: 0, completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
showCert: {}, showCert: {},
@ -48,6 +49,7 @@ const initialState = {
}, },
sessionMeta: { activeDonations: 0 }, sessionMeta: { activeDonations: 0 },
showDonationModal: false, showDonationModal: false,
isBlockDonationModal: false,
isOnline: true isOnline: true
}; };
@ -55,9 +57,10 @@ export const types = createTypes(
[ [
'appMount', 'appMount',
'hardGoTo', 'hardGoTo',
'allowDonationRequests', 'allowBlockDonationRequests',
'closeDonationModal', 'closeDonationModal',
'preventDonationRequests', 'preventBlockDonationRequests',
'preventProgressDonationRequests',
'openDonationModal', 'openDonationModal',
'onlineStatusChange', 'onlineStatusChange',
'resetUserData', 'resetUserData',
@ -92,11 +95,16 @@ export const appMount = createAction(types.appMount);
export const tryToShowDonationModal = createAction( export const tryToShowDonationModal = createAction(
types.tryToShowDonationModal types.tryToShowDonationModal
); );
export const allowDonationRequests = createAction(types.allowDonationRequests); export const allowBlockDonationRequests = createAction(
types.allowBlockDonationRequests
);
export const closeDonationModal = createAction(types.closeDonationModal); export const closeDonationModal = createAction(types.closeDonationModal);
export const openDonationModal = createAction(types.openDonationModal); export const openDonationModal = createAction(types.openDonationModal);
export const preventDonationRequests = createAction( export const preventBlockDonationRequests = createAction(
types.preventDonationRequests types.preventBlockDonationRequests
);
export const preventProgressDonationRequests = createAction(
types.preventProgressDonationRequests
); );
export const onlineStatusChange = createAction(types.onlineStatusChange); 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 isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername; export const isSignedInSelector = state => !!state[ns].appUsername;
export const isDonationModalOpenSelector = state => state[ns].showDonationModal; 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 => export const signInLoadingSelector = state =>
userFetchStateSelector(state).pending; userFetchStateSelector(state).pending;
export const showCertSelector = state => state[ns].showCert; export const showCertSelector = state => state[ns].showCert;
export const showCertFetchStateSelector = state => state[ns].showCertFetchState; export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
export const shouldRequestDonationSelector = state => export const shouldRequestDonationSelector = state => {
!isDonatingSelector(state) && state[ns].canRequestDonation; 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 => { export const userByNameSelector = username => state => {
const { user } = state[ns]; const { user } = state[ns];
@ -203,9 +238,9 @@ function spreadThePayloadOnUser(state, payload) {
export const reducer = handleActions( export const reducer = handleActions(
{ {
[types.allowDonationRequests]: state => ({ [types.allowBlockDonationRequests]: state => ({
...state, ...state,
canRequestDonation: true canRequestBlockDonation: true
}), }),
[types.fetchUser]: state => ({ [types.fetchUser]: state => ({
...state, ...state,
@ -282,13 +317,18 @@ export const reducer = handleActions(
...state, ...state,
showDonationModal: false showDonationModal: false
}), }),
[types.openDonationModal]: state => ({ [types.openDonationModal]: (state, { payload }) => ({
...state, ...state,
showDonationModal: true showDonationModal: true,
isBlockDonationModal: payload
}), }),
[types.preventDonationRequests]: state => ({ [types.preventBlockDonationRequests]: state => ({
...state, ...state,
canRequestDonation: false canRequestBlockDonation: false
}),
[types.preventProgressDonationRequests]: state => ({
...state,
canRequestProgressDonation: false
}), }),
[types.resetUserData]: state => ({ [types.resetUserData]: state => ({
...state, ...state,

View File

@ -5,7 +5,7 @@ import {
isSignedInSelector, isSignedInSelector,
updateComplete, updateComplete,
updateFailed, updateFailed,
allowDonationRequests allowBlockDonationRequests
} from '../../../redux'; } from '../../../redux';
import { post } from '../../../utils/ajax'; import { post } from '../../../utils/ajax';
@ -38,14 +38,14 @@ export function* updateSuccessMessageSaga() {
yield put(updateSuccessMessage(randomCompliment())); yield put(updateSuccessMessage(randomCompliment()));
} }
export function* allowDonationRequestsSaga() { export function* allowBlockDonationRequestsSaga() {
yield put(allowDonationRequests()); yield put(allowBlockDonationRequests());
} }
export function createCurrentChallengeSaga(types) { export function createCurrentChallengeSaga(types) {
return [ return [
takeEvery(types.challengeMounted, currentChallengeSaga), takeEvery(types.challengeMounted, currentChallengeSaga),
takeEvery(types.challengeMounted, updateSuccessMessageSaga), takeEvery(types.challengeMounted, updateSuccessMessageSaga),
takeEvery(types.lastBlockChalSubmitted, allowDonationRequestsSaga) takeEvery(types.lastBlockChalSubmitted, allowBlockDonationRequestsSaga)
]; ];
} }

View File

@ -1,12 +1,12 @@
/* global expect */ /* global expect */
import { allowDonationRequestsSaga } from './current-challenge-saga'; import { allowBlockDonationRequestsSaga } from './current-challenge-saga';
import { types as appTypes } from '../../../redux'; import { types as appTypes } from '../../../redux';
describe('allowDonationRequestsSaga', () => { describe('allowBlockDonationRequestsSaga', () => {
it('should call allowDonationRequests', () => { it('should call allowBlockDonationRequests', () => {
const gen = allowDonationRequestsSaga(); const gen = allowBlockDonationRequestsSaga();
expect(gen.next().value.payload.action.type).toEqual( expect(gen.next().value.payload.action.type).toEqual(
appTypes.allowDonationRequests appTypes.allowBlockDonationRequests
); );
}); });
}); });