feat: activate progress based donation modal (#37882)
This commit is contained in:
committed by
mrugesh
parent
f1ddec3f9b
commit
3f075f91d8
59
client/src/assets/icons/Cup.js
Normal file
59
client/src/assets/icons/Cup.js
Normal 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;
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user