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;
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 {

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 { 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 (
<Modal bsSize='lg' className='donation-modal' show={show}>
<Modal.Header className='fcc-modal'>
<Modal.Title className='modal-title text-center'>
<strong>Support freeCodeCamp.org</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<p className='text-center'>
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.Footer>
<Link
className='btn-invert btn btn-lg btn-primary btn-block btn-cta'
onClick={this.props.closeDonationModal}
to={`/donate`}
>
Support our nonprofit
</Link>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='btn-invert'
onClick={this.props.closeDonationModal}
>
Ask me later
</Button>
</Modal.Footer>
</Modal>
);
function DonateModal({
show,
block,
activeDonors,
isBlockDonation,
closeDonationModal
}) {
if (show) {
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 (
<Modal bsSize='lg' className='donation-modal' show={show}>
<Modal.Header className='fcc-modal'>
<Modal.Title className='modal-title text-center'>
<strong>Support freeCodeCamp.org</strong>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{isBlockDonation ? blockDonationText : progressDonationText}
</Modal.Body>
<Modal.Footer>
<Link
className='btn-invert btn btn-lg btn-primary btn-block btn-cta'
onClick={closeDonationModal}
to={`/donate`}
>
Support our nonprofit
</Link>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='btn-invert'
onClick={closeDonationModal}
>
Ask me later
</Button>
</Modal.Footer>
</Modal>
);
}
DonateModal.displayName = 'DonateModal';

View File

@ -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());
}
}
}

View File

@ -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,

View File

@ -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)
];
}

View File

@ -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
);
});
});