feat(donate):add donation modal and certification message (#37822)

Co-Authored-By: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2019-12-02 15:48:53 +03:00
committed by GitHub
parent 9866d2f241
commit a9bbcda211
17 changed files with 503 additions and 89 deletions

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

View File

@ -8,7 +8,12 @@ import { Grid, Row, Col, Image } from '@freecodecamp/react-bootstrap';
import { import {
showCertSelector, showCertSelector,
showCertFetchStateSelector, showCertFetchStateSelector,
showCert showCert,
userFetchStateSelector,
usernameSelector,
isDonatingSelector,
isDonationRequestedSelector,
preventDonationRequests
} from '../redux'; } from '../redux';
import validCertNames from '../../utils/validCertNames'; import validCertNames from '../../utils/validCertNames';
import { createFlashMessage } from '../components/Flash/redux'; import { createFlashMessage } from '../components/Flash/redux';
@ -16,7 +21,7 @@ import standardErrorMessage from '../utils/standardErrorMessage';
import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage'; import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage';
import RedirectHome from '../components/RedirectHome'; import RedirectHome from '../components/RedirectHome';
import { Loader } from '../components/helpers'; import { Loader, Link } from '../components/helpers';
const propTypes = { const propTypes = {
cert: PropTypes.shape({ cert: PropTypes.shape({
@ -35,8 +40,15 @@ const propTypes = {
complete: PropTypes.bool, complete: PropTypes.bool,
errored: PropTypes.bool errored: PropTypes.bool
}), }),
isDonating: PropTypes.bool,
isDonationRequested: PropTypes.bool,
issueDate: PropTypes.string, issueDate: PropTypes.string,
preventDonationRequests: PropTypes.func,
showCert: PropTypes.func.isRequired, showCert: PropTypes.func.isRequired,
signedInUserName: PropTypes.string,
userFetchState: PropTypes.shape({
complete: PropTypes.bool
}),
userFullName: PropTypes.string, userFullName: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
validCertName: PropTypes.bool validCertName: PropTypes.bool
@ -47,16 +59,34 @@ const mapStateToProps = (state, { certName }) => {
return createSelector( return createSelector(
showCertSelector, showCertSelector,
showCertFetchStateSelector, showCertFetchStateSelector,
(cert, fetchState) => ({ usernameSelector,
userFetchStateSelector,
isDonatingSelector,
isDonationRequestedSelector,
(
cert, cert,
fetchState, fetchState,
validCertName signedInUserName,
userFetchState,
isDonating,
isDonationRequested
) => ({
cert,
fetchState,
validCertName,
signedInUserName,
userFetchState,
isDonating,
isDonationRequested
}) })
); );
}; };
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators({ createFlashMessage, showCert }, dispatch); bindActionCreators(
{ createFlashMessage, showCert, preventDonationRequests },
dispatch
);
class ShowCertification extends Component { class ShowCertification extends Component {
componentDidMount() { componentDidMount() {
@ -72,7 +102,12 @@ class ShowCertification extends Component {
fetchState, fetchState,
validCertName, validCertName,
createFlashMessage, createFlashMessage,
certName certName,
preventDonationRequests,
signedInUserName,
isDonating,
isDonationRequested,
userFetchState
} = this.props; } = this.props;
if (!validCertName) { if (!validCertName) {
@ -81,6 +116,7 @@ class ShowCertification extends Component {
} }
const { pending, complete, errored } = fetchState; const { pending, complete, errored } = fetchState;
const { complete: userComplete } = userFetchState;
if (pending) { if (pending) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
@ -103,8 +139,40 @@ class ShowCertification extends Component {
certTitle, certTitle,
completionTime completionTime
} = cert; } = 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. Its no easy task. Running freeCodeCamp isnt 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 ( return (
<div className='certificate-outer-wrapper'> <div className='certificate-outer-wrapper'>
{conditionalDonationMessage}
<Grid className='certificate-wrapper certification-namespace'> <Grid className='certificate-wrapper certification-namespace'>
<Row> <Row>
<header> <header>

View File

@ -162,3 +162,59 @@ li.disabled > a {
.servicebot-embed-panel .panel { .servicebot-embed-panel .panel {
padding: 20px; 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;
}
}

View 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);

View File

@ -1,11 +1,42 @@
import React, { Fragment } from 'react'; import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
function CertificationLayout({ children }) { import ga from '../../analytics';
return <Fragment>{children}</Fragment>; 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.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);

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react'; import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -7,9 +7,11 @@ import { Loader } from '../../components/helpers';
import { import {
userSelector, userSelector,
userFetchStateSelector, userFetchStateSelector,
isSignedInSelector isSignedInSelector,
tryToShowDonationModal
} from '../../redux'; } from '../../redux';
import createRedirect from '../../components/createRedirect'; import createRedirect from '../../components/createRedirect';
import DonateModal from '../Donation/components/DonationModal';
import 'prismjs/themes/prism.css'; import 'prismjs/themes/prism.css';
import './prism.css'; import './prism.css';
@ -28,14 +30,25 @@ const mapStateToProps = createSelector(
}) })
); );
const mapDispatchToProps = {
tryToShowDonationModal
};
const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms'); const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms');
function LearnLayout({ class LearnLayout extends Component {
componentDidMount() {
this.props.tryToShowDonationModal();
}
render() {
const {
fetchState: { pending, complete }, fetchState: { pending, complete },
isSignedIn, isSignedIn,
user: { acceptedPrivacyTerms }, user: { acceptedPrivacyTerms },
children children
}) { } = this.props;
if (pending && !complete) { if (pending && !complete) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
} }
@ -47,8 +60,10 @@ function LearnLayout({
return ( return (
<Fragment> <Fragment>
<main id='learn-app-wrapper'>{children}</main> <main id='learn-app-wrapper'>{children}</main>
<DonateModal />
</Fragment> </Fragment>
); );
}
} }
LearnLayout.displayName = 'LearnLayout'; LearnLayout.displayName = 'LearnLayout';
@ -60,9 +75,13 @@ LearnLayout.propTypes = {
errored: PropTypes.bool errored: PropTypes.bool
}), }),
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
tryToShowDonationModal: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
acceptedPrivacyTerms: PropTypes.bool acceptedPrivacyTerms: PropTypes.bool
}) })
}; };
export default connect(mapStateToProps)(LearnLayout); export default connect(
mapStateToProps,
mapDispatchToProps
)(LearnLayout);

View File

@ -208,18 +208,19 @@ fieldset[disabled] .btn-primary.focus {
} }
.btn-cta { .btn-cta {
background-color: #ffac33; background-color: #feac32;
background-image: linear-gradient(#ffcc4d, #ffac33); background-image: linear-gradient(#fecc4c, #ffac33);
border-color: #f1a02a; border-width: 3px;
border-color: #feac32;
color: #0a0a23 !important; color: #0a0a23 !important;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
border: none;
} }
.btn-cta:hover, .btn-cta:hover,
.btn-cta:focus, .btn-cta:focus,
.btn-cta:active:hover { .btn-cta:active:hover {
background-color: #e99110; background-color: #fecc4c;
background-image: linear-gradient(#ffcc4d, #e99110); border-width: 3px;
border-color: #f1a02a;
background-image: none;
color: #0a0a23 !important; color: #0a0a23 !important;
} }
.btn-cta:active { .btn-cta:active {

View File

@ -19,6 +19,8 @@
--green-dark: #00471b; --green-dark: #00471b;
--red-light: #ffadad; --red-light: #ffadad;
--red-dark: #850000; --red-dark: #850000;
--love-light: #f8577c;
--love-dark: #f82153;
} }
.dark-palette { .dark-palette {
@ -36,6 +38,7 @@
--success-background: var(--green-dark); --success-background: var(--green-dark);
--danger-color: var(--red-light); --danger-color: var(--red-light);
--danger-background: var(--red-dark); --danger-background: var(--red-dark);
--love-color: var(--love-light);
} }
.light-palette { .light-palette {
@ -53,4 +56,5 @@
--success-background: var(--green-light); --success-background: var(--green-light);
--danger-color: var(--red-dark); --danger-color: var(--red-dark);
--danger-background: var(--red-light); --danger-background: var(--red-light);
--love-color: var(--love-dark);
} }

View File

@ -35,6 +35,23 @@
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 100vh; 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 { .certification-namespace header {

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

View File

@ -10,6 +10,7 @@ import { createAppMountSaga } from './app-mount-saga';
import { createReportUserSaga } from './report-user-saga'; import { createReportUserSaga } from './report-user-saga';
import { createShowCertSaga } from './show-cert-saga'; import { createShowCertSaga } from './show-cert-saga';
import { createNightModeSaga } from './night-mode-saga'; import { createNightModeSaga } from './night-mode-saga';
import { createDonationSaga } from './donation-saga';
import hardGoToEpic from './hard-go-to-epic'; import hardGoToEpic from './hard-go-to-epic';
import failedUpdatesEpic from './failed-updates-epic'; import failedUpdatesEpic from './failed-updates-epic';
@ -31,6 +32,7 @@ export const defaultFetchState = {
const initialState = { const initialState = {
appUsername: '', appUsername: '',
canRequestDonation: false,
completionCount: 0, completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY), currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
donationRequested: false, donationRequested: false,
@ -53,12 +55,14 @@ const initialState = {
export const types = createTypes( export const types = createTypes(
[ [
'appMount', 'appMount',
'closeDonationModal',
'donationRequested',
'hardGoTo', 'hardGoTo',
'allowDonationRequests',
'closeDonationModal',
'preventDonationRequests',
'openDonationModal', 'openDonationModal',
'onlineStatusChange', 'onlineStatusChange',
'resetUserData', 'resetUserData',
'tryToShowDonationModal',
'submitComplete', 'submitComplete',
'updateComplete', 'updateComplete',
'updateCurrentChallengeId', 'updateCurrentChallengeId',
@ -77,6 +81,7 @@ export const epics = [hardGoToEpic, failedUpdatesEpic, updateCompleteEpic];
export const sagas = [ export const sagas = [
...createAcceptTermsSaga(types), ...createAcceptTermsSaga(types),
...createAppMountSaga(types), ...createAppMountSaga(types),
...createDonationSaga(types),
...createFetchUserSaga(types), ...createFetchUserSaga(types),
...createShowCertSaga(types), ...createShowCertSaga(types),
...createReportUserSaga(types), ...createReportUserSaga(types),
@ -85,9 +90,15 @@ export const sagas = [
export const appMount = createAction(types.appMount); 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 closeDonationModal = createAction(types.closeDonationModal);
export const openDonationModal = createAction(types.openDonationModal); export const openDonationModal = createAction(types.openDonationModal);
export const donationRequested = createAction(types.donationRequested); export const preventDonationRequests = createAction(
types.preventDonationRequests
);
export const onlineStatusChange = createAction(types.onlineStatusChange); export const onlineStatusChange = createAction(types.onlineStatusChange);
@ -134,7 +145,8 @@ export const completedChallengesSelector = state =>
userSelector(state).completedChallenges || []; userSelector(state).completedChallenges || [];
export const completionCountSelector = state => state[ns].completionCount; export const completionCountSelector = state => state[ns].completionCount;
export const currentChallengeIdSelector = state => state[ns].currentChallengeId; 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 isOnlineSelector = state => state[ns].isOnline;
export const isSignedInSelector = state => !!state[ns].appUsername; export const isSignedInSelector = state => !!state[ns].appUsername;
@ -145,21 +157,14 @@ export const signInLoadingSelector = state =>
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 showDonationSelector = state => { export const shouldRequestDonationSelector = state => {
const completedChallenges = completedChallengesSelector(state); const isDonationRequested = isDonationRequestedSelector(state);
const completionCount = completionCountSelector(state); const isDonating = isDonatingSelector(state);
const currentCompletedLength = completedChallenges.length; if (
const donationRequested = donationRequestedSelector(state); isDonationRequested === false &&
// the user has not completed 9 challenges in total yet isDonating === false &&
if (currentCompletedLength < 9) { state[ns].canRequestDonation
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) {
return true; return true;
} }
return false; return false;
@ -209,6 +214,10 @@ function spreadThePayloadOnUser(state, payload) {
export const reducer = handleActions( export const reducer = handleActions(
{ {
[types.allowDonationRequests]: state => ({
...state,
canRequestDonation: true
}),
[types.fetchUser]: state => ({ [types.fetchUser]: state => ({
...state, ...state,
userFetchState: { ...defaultFetchState } userFetchState: { ...defaultFetchState }
@ -288,7 +297,7 @@ export const reducer = handleActions(
...state, ...state,
showDonationModal: true showDonationModal: true
}), }),
[types.donationRequested]: state => ({ [types.preventDonationRequests]: state => ({
...state, ...state,
donationRequested: true donationRequested: true
}), }),

View File

@ -21,7 +21,8 @@ import {
isCompletionModalOpenSelector, isCompletionModalOpenSelector,
successMessageSelector, successMessageSelector,
challengeFilesSelector, challengeFilesSelector,
challengeMetaSelector challengeMetaSelector,
lastBlockChalSubmitted
} from '../redux'; } from '../redux';
import { isSignedInSelector } from '../../../redux'; import { isSignedInSelector } from '../../../redux';
@ -54,17 +55,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = function(dispatch) { const mapDispatchToProps = function(dispatch) {
const dispatchers = { const dispatchers = {
close: () => dispatch(closeModal('completion')), 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: () => { submitChallenge: () => {
dispatch(submitChallenge()); dispatch(submitChallenge());
},
lastBlockChalSubmitted: () => {
dispatch(lastBlockChalSubmitted());
} }
}; };
return () => dispatchers; return () => dispatchers;
@ -76,18 +71,18 @@ const propTypes = {
completedChallengesIds: PropTypes.array, completedChallengesIds: PropTypes.array,
currentBlockIds: PropTypes.array, currentBlockIds: PropTypes.array,
files: PropTypes.object.isRequired, files: PropTypes.object.isRequired,
handleKeypress: PropTypes.func.isRequired,
id: PropTypes.string, id: PropTypes.string,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
isSignedIn: PropTypes.bool.isRequired, isSignedIn: PropTypes.bool.isRequired,
lastBlockChalSubmitted: PropTypes.func,
message: PropTypes.string, message: PropTypes.string,
submitChallenge: PropTypes.func.isRequired, submitChallenge: PropTypes.func.isRequired,
title: PropTypes.string title: PropTypes.string
}; };
export function getCompletedPercent( export function getCompletedPercent(
completedChallengesIds, completedChallengesIds = [],
currentBlockIds, currentBlockIds = [],
currentChallengeId currentChallengeId
) { ) {
completedChallengesIds = completedChallengesIds.includes(currentChallengeId) completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
@ -106,8 +101,15 @@ export function getCompletedPercent(
} }
export class CompletionModalInner extends Component { export class CompletionModalInner extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleKeypress = this.handleKeypress.bind(this);
}
state = { state = {
downloadURL: null downloadURL: null,
completedPercent: 0
}; };
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
@ -135,7 +137,37 @@ export class CompletionModalInner extends Component {
}); });
newURL = URL.createObjectURL(blob); 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() { componentWillUnmount() {
@ -149,20 +181,13 @@ export class CompletionModalInner extends Component {
const { const {
blockName = '', blockName = '',
close, close,
completedChallengesIds = [],
currentBlockIds = [],
id = '',
isOpen, isOpen,
isSignedIn,
submitChallenge,
handleKeypress,
message, message,
title title,
isSignedIn
} = this.props; } = this.props;
const completedPercent = !isSignedIn const { completedPercent } = this.state;
? 0
: getCompletedPercent(completedChallengesIds, currentBlockIds, id);
if (isOpen) { if (isOpen) {
ga.modalview('/completion-modal'); ga.modalview('/completion-modal');
@ -175,7 +200,7 @@ export class CompletionModalInner extends Component {
dialogClassName='challenge-success-modal' dialogClassName='challenge-success-modal'
keyboard={true} keyboard={true}
onHide={close} onHide={close}
onKeyDown={isOpen ? handleKeypress : noop} onKeyDown={isOpen ? this.handleKeypress : noop}
show={isOpen} show={isOpen}
> >
<Modal.Header <Modal.Header
@ -195,7 +220,7 @@ export class CompletionModalInner extends Component {
block={true} block={true}
bsSize='large' bsSize='large'
bsStyle='primary' bsStyle='primary'
onClick={submitChallenge} onClick={this.handleSubmit}
> >
{isSignedIn ? 'Submit and g' : 'G'}o to next challenge{' '} {isSignedIn ? 'Submit and g' : 'G'}o to next challenge{' '}
<span className='hidden-xs'>(Ctrl + Enter)</span> <span className='hidden-xs'>(Ctrl + Enter)</span>

View File

@ -132,6 +132,7 @@ export default function completionEpic(action$, state$) {
const meta = challengeMetaSelector(state); const meta = challengeMetaSelector(state);
const { nextChallengePath, introPath, challengeType } = meta; const { nextChallengePath, introPath, challengeType } = meta;
const closeChallengeModal = of(closeModal('completion')); const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' }); let submitter = () => of({ type: 'no-user-signed-in' });
if ( if (
!(challengeType in submitTypes) || !(challengeType in submitTypes) ||

View File

@ -4,7 +4,8 @@ import store from 'store';
import { import {
isSignedInSelector, isSignedInSelector,
updateComplete, updateComplete,
updateFailed updateFailed,
allowDonationRequests
} from '../../../redux'; } from '../../../redux';
import { post } from '../../../utils/ajax'; import { post } from '../../../utils/ajax';
@ -33,13 +34,18 @@ export function* currentChallengeSaga({ payload: id }) {
} }
} }
function* updateSuccessMessageSaga() { export function* updateSuccessMessageSaga() {
yield put(updateSuccessMessage(randomCompliment())); yield put(updateSuccessMessage(randomCompliment()));
} }
export function* allowDonationRequestsSaga() {
yield put(allowDonationRequests());
}
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)
]; ];
} }

View File

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

View File

@ -30,6 +30,7 @@ const initialState = {
}, },
challengeTests: [], challengeTests: [],
consoleOut: '', consoleOut: '',
hasCompletedBlock: false,
inAccessibilityMode: false, inAccessibilityMode: false,
isCodeLocked: false, isCodeLocked: false,
isBuildEnabled: true, isBuildEnabled: true,
@ -81,7 +82,9 @@ export const types = createTypes(
'moveToTab', 'moveToTab',
'setEditorFocusability', 'setEditorFocusability',
'setAccessibilityMode' 'setAccessibilityMode',
'lastBlockChalSubmitted'
], ],
ns ns
); );
@ -157,6 +160,10 @@ export const moveToTab = createAction(types.moveToTab);
export const setEditorFocusability = createAction(types.setEditorFocusability); export const setEditorFocusability = createAction(types.setEditorFocusability);
export const setAccessibilityMode = createAction(types.setAccessibilityMode); export const setAccessibilityMode = createAction(types.setAccessibilityMode);
export const lastBlockChalSubmitted = createAction(
types.lastBlockChalSubmitted
);
export const currentTabSelector = state => state[ns].currentTab; export const currentTabSelector = state => state[ns].currentTab;
export const challengeFilesSelector = state => state[ns].challengeFiles; export const challengeFilesSelector = state => state[ns].challengeFiles;
export const challengeMetaSelector = state => state[ns].challengeMeta; export const challengeMetaSelector = state => state[ns].challengeMeta;

View File

@ -15,7 +15,9 @@ export default function layoutSelector({ element, props }) {
return <DefaultLayout pathname={pathname}>{element}</DefaultLayout>; return <DefaultLayout pathname={pathname}>{element}</DefaultLayout>;
} }
if (/^\/certification(\/.*)*/.test(pathname)) { if (/^\/certification(\/.*)*/.test(pathname)) {
return <CertificationLayout>{element}</CertificationLayout>; return (
<CertificationLayout pathname={pathname}>{element}</CertificationLayout>
);
} }
if (/^\/guide(\/.*)*/.test(pathname)) { if (/^\/guide(\/.*)*/.test(pathname)) {
console.log('Hitting guide for some reason. Need a redirect.'); console.log('Hitting guide for some reason. Need a redirect.');