feat(donate):add donation modal and certification message (#37822)
Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
38
client/src/assets/icons/Heart.js
Normal file
38
client/src/assets/icons/Heart.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
const propTypes = {};
|
||||
|
||||
function Heart(props) {
|
||||
return (
|
||||
<Fragment>
|
||||
<span className='sr-only'>Heart</span>
|
||||
<svg
|
||||
height={184}
|
||||
version='1.1'
|
||||
viewBox='0 0 200 184'
|
||||
width={200}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<g fill='none' fillRule='evenodd'>
|
||||
<g fill='var(--love-color)'>
|
||||
<ellipse cx='140.5' cy={59} id='a' rx='59.5' ry={59} />
|
||||
<circle cx={59} cy={59} r={59} />
|
||||
<rect
|
||||
height={118}
|
||||
transform='translate(100 100) rotate(-45) translate(-100 -100)'
|
||||
width={118}
|
||||
x={41}
|
||||
y={41}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Heart.displayName = 'Heart';
|
||||
Heart.propTypes = propTypes;
|
||||
|
||||
export default Heart;
|
@ -8,7 +8,12 @@ import { Grid, Row, Col, Image } from '@freecodecamp/react-bootstrap';
|
||||
import {
|
||||
showCertSelector,
|
||||
showCertFetchStateSelector,
|
||||
showCert
|
||||
showCert,
|
||||
userFetchStateSelector,
|
||||
usernameSelector,
|
||||
isDonatingSelector,
|
||||
isDonationRequestedSelector,
|
||||
preventDonationRequests
|
||||
} from '../redux';
|
||||
import validCertNames from '../../utils/validCertNames';
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
@ -16,7 +21,7 @@ import standardErrorMessage from '../utils/standardErrorMessage';
|
||||
import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage';
|
||||
|
||||
import RedirectHome from '../components/RedirectHome';
|
||||
import { Loader } from '../components/helpers';
|
||||
import { Loader, Link } from '../components/helpers';
|
||||
|
||||
const propTypes = {
|
||||
cert: PropTypes.shape({
|
||||
@ -35,8 +40,15 @@ const propTypes = {
|
||||
complete: PropTypes.bool,
|
||||
errored: PropTypes.bool
|
||||
}),
|
||||
isDonating: PropTypes.bool,
|
||||
isDonationRequested: PropTypes.bool,
|
||||
issueDate: PropTypes.string,
|
||||
preventDonationRequests: PropTypes.func,
|
||||
showCert: PropTypes.func.isRequired,
|
||||
signedInUserName: PropTypes.string,
|
||||
userFetchState: PropTypes.shape({
|
||||
complete: PropTypes.bool
|
||||
}),
|
||||
userFullName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
validCertName: PropTypes.bool
|
||||
@ -47,16 +59,34 @@ const mapStateToProps = (state, { certName }) => {
|
||||
return createSelector(
|
||||
showCertSelector,
|
||||
showCertFetchStateSelector,
|
||||
(cert, fetchState) => ({
|
||||
usernameSelector,
|
||||
userFetchStateSelector,
|
||||
isDonatingSelector,
|
||||
isDonationRequestedSelector,
|
||||
(
|
||||
cert,
|
||||
fetchState,
|
||||
validCertName
|
||||
signedInUserName,
|
||||
userFetchState,
|
||||
isDonating,
|
||||
isDonationRequested
|
||||
) => ({
|
||||
cert,
|
||||
fetchState,
|
||||
validCertName,
|
||||
signedInUserName,
|
||||
userFetchState,
|
||||
isDonating,
|
||||
isDonationRequested
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ createFlashMessage, showCert }, dispatch);
|
||||
bindActionCreators(
|
||||
{ createFlashMessage, showCert, preventDonationRequests },
|
||||
dispatch
|
||||
);
|
||||
|
||||
class ShowCertification extends Component {
|
||||
componentDidMount() {
|
||||
@ -72,7 +102,12 @@ class ShowCertification extends Component {
|
||||
fetchState,
|
||||
validCertName,
|
||||
createFlashMessage,
|
||||
certName
|
||||
certName,
|
||||
preventDonationRequests,
|
||||
signedInUserName,
|
||||
isDonating,
|
||||
isDonationRequested,
|
||||
userFetchState
|
||||
} = this.props;
|
||||
|
||||
if (!validCertName) {
|
||||
@ -81,6 +116,7 @@ class ShowCertification extends Component {
|
||||
}
|
||||
|
||||
const { pending, complete, errored } = fetchState;
|
||||
const { complete: userComplete } = userFetchState;
|
||||
|
||||
if (pending) {
|
||||
return <Loader fullScreen={true} />;
|
||||
@ -103,8 +139,40 @@ class ShowCertification extends Component {
|
||||
certTitle,
|
||||
completionTime
|
||||
} = cert;
|
||||
|
||||
let conditionalDonationMessage = '';
|
||||
|
||||
if (
|
||||
userComplete &&
|
||||
signedInUserName === username &&
|
||||
!isDonating &&
|
||||
!isDonationRequested
|
||||
) {
|
||||
conditionalDonationMessage = (
|
||||
<Grid>
|
||||
<Row className='certification-donation text-center'>
|
||||
<p>
|
||||
Only you can see this message. Congratulations on earning this
|
||||
certification. It’s no easy task. Running freeCodeCamp isn’t easy
|
||||
either. Nor is it cheap. Help us help you and many other people
|
||||
around the world. Make a tax-deductible supporting donation to our
|
||||
nonprofit today.
|
||||
</p>
|
||||
<Link
|
||||
className={'btn'}
|
||||
onClick={preventDonationRequests}
|
||||
to={'/donate'}
|
||||
>
|
||||
Check out our donation dashboard
|
||||
</Link>
|
||||
</Row>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='certificate-outer-wrapper'>
|
||||
{conditionalDonationMessage}
|
||||
<Grid className='certificate-wrapper certification-namespace'>
|
||||
<Row>
|
||||
<header>
|
||||
|
@ -162,3 +162,59 @@ li.disabled > a {
|
||||
.servicebot-embed-panel .panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.heart-icon-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.heart-icon {
|
||||
width: 150px;
|
||||
height: auto;
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
animation: heart-icon-animation 1s linear 100ms forwards;
|
||||
}
|
||||
|
||||
@keyframes heart-icon-animation {
|
||||
33% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
66% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.donation-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.donation-modal .modal-title {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.heart-icon-container {
|
||||
margin: 30px;
|
||||
}
|
||||
.donation-modal p {
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.donation-modal .modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
98
client/src/components/Donation/components/DonationModal.js
Normal file
98
client/src/components/Donation/components/DonationModal.js
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Modal, Button } from '@freecodecamp/react-bootstrap';
|
||||
import { Link } from '../../../components/helpers';
|
||||
import { blockNameify } from '../../../../utils/blockNameify';
|
||||
import Heart from '../../../assets/icons/Heart';
|
||||
|
||||
import ga from '../../../analytics';
|
||||
import {
|
||||
closeDonationModal,
|
||||
isDonationModalOpenSelector
|
||||
} from '../../../redux';
|
||||
|
||||
import { challengeMetaSelector } from '../../../templates/Challenges/redux';
|
||||
|
||||
import '../Donation.css';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isDonationModalOpenSelector,
|
||||
challengeMetaSelector,
|
||||
(show, { block }) => ({
|
||||
show,
|
||||
block
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
closeDonationModal
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const propTypes = {
|
||||
block: PropTypes.string,
|
||||
closeDonationModal: PropTypes.func.isRequired,
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DonateModal.displayName = 'DonateModal';
|
||||
DonateModal.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DonateModal);
|
@ -1,11 +1,42 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
function CertificationLayout({ children }) {
|
||||
return <Fragment>{children}</Fragment>;
|
||||
import ga from '../../analytics';
|
||||
import { fetchUser, isSignedInSelector } from '../../redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
isSignedIn => ({
|
||||
isSignedIn
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = { fetchUser };
|
||||
|
||||
class CertificationLayout extends Component {
|
||||
componentDidMount() {
|
||||
const { isSignedIn, fetchUser, pathname } = this.props;
|
||||
if (!isSignedIn) {
|
||||
fetchUser();
|
||||
}
|
||||
ga.pageview(pathname);
|
||||
}
|
||||
render() {
|
||||
return <Fragment>{this.props.children}</Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
CertificationLayout.displayName = 'CertificationLayout';
|
||||
CertificationLayout.propTypes = { children: PropTypes.any };
|
||||
CertificationLayout.propTypes = {
|
||||
children: PropTypes.any,
|
||||
fetchUser: PropTypes.func.isRequired,
|
||||
isSignedIn: PropTypes.bool,
|
||||
pathname: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default CertificationLayout;
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CertificationLayout);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
@ -7,9 +7,11 @@ import { Loader } from '../../components/helpers';
|
||||
import {
|
||||
userSelector,
|
||||
userFetchStateSelector,
|
||||
isSignedInSelector
|
||||
isSignedInSelector,
|
||||
tryToShowDonationModal
|
||||
} from '../../redux';
|
||||
import createRedirect from '../../components/createRedirect';
|
||||
import DonateModal from '../Donation/components/DonationModal';
|
||||
|
||||
import 'prismjs/themes/prism.css';
|
||||
import './prism.css';
|
||||
@ -28,27 +30,40 @@ const mapStateToProps = createSelector(
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = {
|
||||
tryToShowDonationModal
|
||||
};
|
||||
|
||||
const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms');
|
||||
|
||||
function LearnLayout({
|
||||
fetchState: { pending, complete },
|
||||
isSignedIn,
|
||||
user: { acceptedPrivacyTerms },
|
||||
children
|
||||
}) {
|
||||
if (pending && !complete) {
|
||||
return <Loader fullScreen={true} />;
|
||||
class LearnLayout extends Component {
|
||||
componentDidMount() {
|
||||
this.props.tryToShowDonationModal();
|
||||
}
|
||||
|
||||
if (isSignedIn && !acceptedPrivacyTerms) {
|
||||
return <RedirectAcceptPrivacyTerm />;
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
fetchState: { pending, complete },
|
||||
isSignedIn,
|
||||
user: { acceptedPrivacyTerms },
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main id='learn-app-wrapper'>{children}</main>
|
||||
</Fragment>
|
||||
);
|
||||
if (pending && !complete) {
|
||||
return <Loader fullScreen={true} />;
|
||||
}
|
||||
|
||||
if (isSignedIn && !acceptedPrivacyTerms) {
|
||||
return <RedirectAcceptPrivacyTerm />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<main id='learn-app-wrapper'>{children}</main>
|
||||
<DonateModal />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LearnLayout.displayName = 'LearnLayout';
|
||||
@ -60,9 +75,13 @@ LearnLayout.propTypes = {
|
||||
errored: PropTypes.bool
|
||||
}),
|
||||
isSignedIn: PropTypes.bool,
|
||||
tryToShowDonationModal: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
acceptedPrivacyTerms: PropTypes.bool
|
||||
})
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(LearnLayout);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LearnLayout);
|
||||
|
@ -208,18 +208,19 @@ fieldset[disabled] .btn-primary.focus {
|
||||
}
|
||||
|
||||
.btn-cta {
|
||||
background-color: #ffac33;
|
||||
background-image: linear-gradient(#ffcc4d, #ffac33);
|
||||
border-color: #f1a02a;
|
||||
background-color: #feac32;
|
||||
background-image: linear-gradient(#fecc4c, #ffac33);
|
||||
border-width: 3px;
|
||||
border-color: #feac32;
|
||||
color: #0a0a23 !important;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
border: none;
|
||||
}
|
||||
.btn-cta:hover,
|
||||
.btn-cta:focus,
|
||||
.btn-cta:active:hover {
|
||||
background-color: #e99110;
|
||||
background-image: linear-gradient(#ffcc4d, #e99110);
|
||||
background-color: #fecc4c;
|
||||
border-width: 3px;
|
||||
border-color: #f1a02a;
|
||||
background-image: none;
|
||||
color: #0a0a23 !important;
|
||||
}
|
||||
.btn-cta:active {
|
||||
|
@ -19,6 +19,8 @@
|
||||
--green-dark: #00471b;
|
||||
--red-light: #ffadad;
|
||||
--red-dark: #850000;
|
||||
--love-light: #f8577c;
|
||||
--love-dark: #f82153;
|
||||
}
|
||||
|
||||
.dark-palette {
|
||||
@ -36,6 +38,7 @@
|
||||
--success-background: var(--green-dark);
|
||||
--danger-color: var(--red-light);
|
||||
--danger-background: var(--red-dark);
|
||||
--love-color: var(--love-light);
|
||||
}
|
||||
|
||||
.light-palette {
|
||||
@ -53,4 +56,5 @@
|
||||
--success-background: var(--green-light);
|
||||
--danger-color: var(--red-dark);
|
||||
--danger-background: var(--red-light);
|
||||
--love-color: var(--love-dark);
|
||||
}
|
||||
|
@ -35,6 +35,23 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.certificate-outer-wrapper .certification-donation {
|
||||
padding: 15px 0px;
|
||||
}
|
||||
|
||||
.certificate-outer-wrapper .certification-donation .btn {
|
||||
background-color: var(--gray-15);
|
||||
border-color: var(--gray-85);
|
||||
color: var(--gray-85);
|
||||
}
|
||||
|
||||
.certificate-outer-wrapper .certification-donation .btn:hover {
|
||||
border-color: var(--gray-85);
|
||||
background-color: var(--gray-85);
|
||||
color: var(--gray-05);
|
||||
}
|
||||
|
||||
.certification-namespace header {
|
||||
|
20
client/src/redux/donation-saga.js
Normal file
20
client/src/redux/donation-saga.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { put, select, takeEvery, delay } from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
openDonationModal,
|
||||
shouldRequestDonationSelector,
|
||||
preventDonationRequests
|
||||
} from './';
|
||||
|
||||
function* showDonateModalSaga() {
|
||||
let shouldRequestDonation = yield select(shouldRequestDonationSelector);
|
||||
if (shouldRequestDonation) {
|
||||
yield delay(200);
|
||||
yield put(openDonationModal());
|
||||
yield put(preventDonationRequests());
|
||||
}
|
||||
}
|
||||
|
||||
export function createDonationSaga(types) {
|
||||
return [takeEvery(types.tryToShowDonationModal, showDonateModalSaga)];
|
||||
}
|
@ -10,6 +10,7 @@ import { createAppMountSaga } from './app-mount-saga';
|
||||
import { createReportUserSaga } from './report-user-saga';
|
||||
import { createShowCertSaga } from './show-cert-saga';
|
||||
import { createNightModeSaga } from './night-mode-saga';
|
||||
import { createDonationSaga } from './donation-saga';
|
||||
|
||||
import hardGoToEpic from './hard-go-to-epic';
|
||||
import failedUpdatesEpic from './failed-updates-epic';
|
||||
@ -31,6 +32,7 @@ export const defaultFetchState = {
|
||||
|
||||
const initialState = {
|
||||
appUsername: '',
|
||||
canRequestDonation: false,
|
||||
completionCount: 0,
|
||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||
donationRequested: false,
|
||||
@ -53,12 +55,14 @@ const initialState = {
|
||||
export const types = createTypes(
|
||||
[
|
||||
'appMount',
|
||||
'closeDonationModal',
|
||||
'donationRequested',
|
||||
'hardGoTo',
|
||||
'allowDonationRequests',
|
||||
'closeDonationModal',
|
||||
'preventDonationRequests',
|
||||
'openDonationModal',
|
||||
'onlineStatusChange',
|
||||
'resetUserData',
|
||||
'tryToShowDonationModal',
|
||||
'submitComplete',
|
||||
'updateComplete',
|
||||
'updateCurrentChallengeId',
|
||||
@ -77,6 +81,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
|
||||
export const sagas = [
|
||||
...createAcceptTermsSaga(types),
|
||||
...createAppMountSaga(types),
|
||||
...createDonationSaga(types),
|
||||
...createFetchUserSaga(types),
|
||||
...createShowCertSaga(types),
|
||||
...createReportUserSaga(types),
|
||||
@ -85,9 +90,15 @@ export const sagas = [
|
||||
|
||||
export const appMount = createAction(types.appMount);
|
||||
|
||||
export const tryToShowDonationModal = createAction(
|
||||
types.tryToShowDonationModal
|
||||
);
|
||||
export const allowDonationRequests = createAction(types.allowDonationRequests);
|
||||
export const closeDonationModal = createAction(types.closeDonationModal);
|
||||
export const openDonationModal = createAction(types.openDonationModal);
|
||||
export const donationRequested = createAction(types.donationRequested);
|
||||
export const preventDonationRequests = createAction(
|
||||
types.preventDonationRequests
|
||||
);
|
||||
|
||||
export const onlineStatusChange = createAction(types.onlineStatusChange);
|
||||
|
||||
@ -134,7 +145,8 @@ export const completedChallengesSelector = state =>
|
||||
userSelector(state).completedChallenges || [];
|
||||
export const completionCountSelector = state => state[ns].completionCount;
|
||||
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
|
||||
export const donationRequestedSelector = state => state[ns].donationRequested;
|
||||
export const isDonationRequestedSelector = state => state[ns].donationRequested;
|
||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||
|
||||
export const isOnlineSelector = state => state[ns].isOnline;
|
||||
export const isSignedInSelector = state => !!state[ns].appUsername;
|
||||
@ -145,21 +157,14 @@ export const signInLoadingSelector = state =>
|
||||
export const showCertSelector = state => state[ns].showCert;
|
||||
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
|
||||
|
||||
export const showDonationSelector = state => {
|
||||
const completedChallenges = completedChallengesSelector(state);
|
||||
const completionCount = completionCountSelector(state);
|
||||
const currentCompletedLength = completedChallenges.length;
|
||||
const donationRequested = donationRequestedSelector(state);
|
||||
// the user has not completed 9 challenges in total yet
|
||||
if (currentCompletedLength < 9) {
|
||||
return false;
|
||||
}
|
||||
// this will mean we are on the 10th submission in total for the user
|
||||
if (completedChallenges.length === 9 && donationRequested === false) {
|
||||
return true;
|
||||
}
|
||||
// this will mean we are on the 3rd submission for this browser session
|
||||
if (completionCount === 2 && donationRequested === false) {
|
||||
export const shouldRequestDonationSelector = state => {
|
||||
const isDonationRequested = isDonationRequestedSelector(state);
|
||||
const isDonating = isDonatingSelector(state);
|
||||
if (
|
||||
isDonationRequested === false &&
|
||||
isDonating === false &&
|
||||
state[ns].canRequestDonation
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -209,6 +214,10 @@ function spreadThePayloadOnUser(state, payload) {
|
||||
|
||||
export const reducer = handleActions(
|
||||
{
|
||||
[types.allowDonationRequests]: state => ({
|
||||
...state,
|
||||
canRequestDonation: true
|
||||
}),
|
||||
[types.fetchUser]: state => ({
|
||||
...state,
|
||||
userFetchState: { ...defaultFetchState }
|
||||
@ -288,7 +297,7 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
showDonationModal: true
|
||||
}),
|
||||
[types.donationRequested]: state => ({
|
||||
[types.preventDonationRequests]: state => ({
|
||||
...state,
|
||||
donationRequested: true
|
||||
}),
|
||||
|
@ -21,7 +21,8 @@ import {
|
||||
isCompletionModalOpenSelector,
|
||||
successMessageSelector,
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector
|
||||
challengeMetaSelector,
|
||||
lastBlockChalSubmitted
|
||||
} from '../redux';
|
||||
|
||||
import { isSignedInSelector } from '../../../redux';
|
||||
@ -54,17 +55,11 @@ const mapStateToProps = createSelector(
|
||||
const mapDispatchToProps = function(dispatch) {
|
||||
const dispatchers = {
|
||||
close: () => dispatch(closeModal('completion')),
|
||||
handleKeypress: e => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
|
||||
// getting to it.
|
||||
e.stopPropagation();
|
||||
dispatch(submitChallenge());
|
||||
}
|
||||
},
|
||||
submitChallenge: () => {
|
||||
dispatch(submitChallenge());
|
||||
},
|
||||
lastBlockChalSubmitted: () => {
|
||||
dispatch(lastBlockChalSubmitted());
|
||||
}
|
||||
};
|
||||
return () => dispatchers;
|
||||
@ -76,18 +71,18 @@ const propTypes = {
|
||||
completedChallengesIds: PropTypes.array,
|
||||
currentBlockIds: PropTypes.array,
|
||||
files: PropTypes.object.isRequired,
|
||||
handleKeypress: PropTypes.func.isRequired,
|
||||
id: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool.isRequired,
|
||||
lastBlockChalSubmitted: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
submitChallenge: PropTypes.func.isRequired,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export function getCompletedPercent(
|
||||
completedChallengesIds,
|
||||
currentBlockIds,
|
||||
completedChallengesIds = [],
|
||||
currentBlockIds = [],
|
||||
currentChallengeId
|
||||
) {
|
||||
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
|
||||
@ -106,8 +101,15 @@ export function getCompletedPercent(
|
||||
}
|
||||
|
||||
export class CompletionModalInner extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleKeypress = this.handleKeypress.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
downloadURL: null
|
||||
downloadURL: null,
|
||||
completedPercent: 0
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
@ -135,7 +137,37 @@ export class CompletionModalInner extends Component {
|
||||
});
|
||||
newURL = URL.createObjectURL(blob);
|
||||
}
|
||||
return { downloadURL: newURL };
|
||||
|
||||
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
|
||||
let completedPercent = isSignedIn
|
||||
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||
: 0;
|
||||
return { downloadURL: newURL, completedPercent: completedPercent };
|
||||
}
|
||||
|
||||
handleKeypress(e) {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
|
||||
// getting to it.
|
||||
e.stopPropagation();
|
||||
this.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
this.props.submitChallenge();
|
||||
this.checkBlockCompletion();
|
||||
}
|
||||
|
||||
// check block completion for donation
|
||||
checkBlockCompletion() {
|
||||
if (
|
||||
this.state.completedPercent === 100 &&
|
||||
!this.props.completedChallengesIds.includes(this.props.id)
|
||||
) {
|
||||
this.props.lastBlockChalSubmitted();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -149,20 +181,13 @@ export class CompletionModalInner extends Component {
|
||||
const {
|
||||
blockName = '',
|
||||
close,
|
||||
completedChallengesIds = [],
|
||||
currentBlockIds = [],
|
||||
id = '',
|
||||
isOpen,
|
||||
isSignedIn,
|
||||
submitChallenge,
|
||||
handleKeypress,
|
||||
message,
|
||||
title
|
||||
title,
|
||||
isSignedIn
|
||||
} = this.props;
|
||||
|
||||
const completedPercent = !isSignedIn
|
||||
? 0
|
||||
: getCompletedPercent(completedChallengesIds, currentBlockIds, id);
|
||||
const { completedPercent } = this.state;
|
||||
|
||||
if (isOpen) {
|
||||
ga.modalview('/completion-modal');
|
||||
@ -175,7 +200,7 @@ export class CompletionModalInner extends Component {
|
||||
dialogClassName='challenge-success-modal'
|
||||
keyboard={true}
|
||||
onHide={close}
|
||||
onKeyDown={isOpen ? handleKeypress : noop}
|
||||
onKeyDown={isOpen ? this.handleKeypress : noop}
|
||||
show={isOpen}
|
||||
>
|
||||
<Modal.Header
|
||||
@ -195,7 +220,7 @@ export class CompletionModalInner extends Component {
|
||||
block={true}
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
onClick={submitChallenge}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{isSignedIn ? 'Submit and g' : 'G'}o to next challenge{' '}
|
||||
<span className='hidden-xs'>(Ctrl + Enter)</span>
|
||||
|
@ -132,6 +132,7 @@ export default function completionEpic(action$, state$) {
|
||||
const meta = challengeMetaSelector(state);
|
||||
const { nextChallengePath, introPath, challengeType } = meta;
|
||||
const closeChallengeModal = of(closeModal('completion'));
|
||||
|
||||
let submitter = () => of({ type: 'no-user-signed-in' });
|
||||
if (
|
||||
!(challengeType in submitTypes) ||
|
||||
|
@ -4,7 +4,8 @@ import store from 'store';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
updateComplete,
|
||||
updateFailed
|
||||
updateFailed,
|
||||
allowDonationRequests
|
||||
} from '../../../redux';
|
||||
|
||||
import { post } from '../../../utils/ajax';
|
||||
@ -33,13 +34,18 @@ export function* currentChallengeSaga({ payload: id }) {
|
||||
}
|
||||
}
|
||||
|
||||
function* updateSuccessMessageSaga() {
|
||||
export function* updateSuccessMessageSaga() {
|
||||
yield put(updateSuccessMessage(randomCompliment()));
|
||||
}
|
||||
|
||||
export function* allowDonationRequestsSaga() {
|
||||
yield put(allowDonationRequests());
|
||||
}
|
||||
|
||||
export function createCurrentChallengeSaga(types) {
|
||||
return [
|
||||
takeEvery(types.challengeMounted, currentChallengeSaga),
|
||||
takeEvery(types.challengeMounted, updateSuccessMessageSaga)
|
||||
takeEvery(types.challengeMounted, updateSuccessMessageSaga),
|
||||
takeEvery(types.lastBlockChalSubmitted, allowDonationRequestsSaga)
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
/* global expect */
|
||||
import { allowDonationRequestsSaga } from './current-challenge-saga';
|
||||
import { types as appTypes } from '../../../redux';
|
||||
|
||||
describe('allowDonationRequestsSaga', () => {
|
||||
it('should call allowDonationRequests', () => {
|
||||
const gen = allowDonationRequestsSaga();
|
||||
expect(gen.next().value.payload.action.type).toEqual(
|
||||
appTypes.allowDonationRequests
|
||||
);
|
||||
});
|
||||
});
|
@ -30,6 +30,7 @@ const initialState = {
|
||||
},
|
||||
challengeTests: [],
|
||||
consoleOut: '',
|
||||
hasCompletedBlock: false,
|
||||
inAccessibilityMode: false,
|
||||
isCodeLocked: false,
|
||||
isBuildEnabled: true,
|
||||
@ -81,7 +82,9 @@ export const types = createTypes(
|
||||
'moveToTab',
|
||||
|
||||
'setEditorFocusability',
|
||||
'setAccessibilityMode'
|
||||
'setAccessibilityMode',
|
||||
|
||||
'lastBlockChalSubmitted'
|
||||
],
|
||||
ns
|
||||
);
|
||||
@ -157,6 +160,10 @@ export const moveToTab = createAction(types.moveToTab);
|
||||
export const setEditorFocusability = createAction(types.setEditorFocusability);
|
||||
export const setAccessibilityMode = createAction(types.setAccessibilityMode);
|
||||
|
||||
export const lastBlockChalSubmitted = createAction(
|
||||
types.lastBlockChalSubmitted
|
||||
);
|
||||
|
||||
export const currentTabSelector = state => state[ns].currentTab;
|
||||
export const challengeFilesSelector = state => state[ns].challengeFiles;
|
||||
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
||||
|
@ -15,7 +15,9 @@ export default function layoutSelector({ element, props }) {
|
||||
return <DefaultLayout pathname={pathname}>{element}</DefaultLayout>;
|
||||
}
|
||||
if (/^\/certification(\/.*)*/.test(pathname)) {
|
||||
return <CertificationLayout>{element}</CertificationLayout>;
|
||||
return (
|
||||
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
|
||||
);
|
||||
}
|
||||
if (/^\/guide(\/.*)*/.test(pathname)) {
|
||||
console.log('Hitting guide for some reason. Need a redirect.');
|
||||
|
Reference in New Issue
Block a user