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;
|
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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user