feat: add minimal form to modal

This commit is contained in:
Ahmad Abdolsaheb
2019-12-09 22:05:09 +01:00
committed by mrugesh
parent 85d3587e59
commit 01d1315835
5 changed files with 260 additions and 79 deletions

View File

@ -18,15 +18,16 @@ import { postChargeStripe } from '../../../utils/ajax';
import { userSelector } from '../../../redux'; import { userSelector } from '../../../redux';
const propTypes = { const propTypes = {
changeCloseBtnLabel: PropTypes.func,
donationAmount: PropTypes.number.isRequired, donationAmount: PropTypes.number.isRequired,
donationDuration: PropTypes.string.isRequired, donationDuration: PropTypes.string.isRequired,
email: PropTypes.string, email: PropTypes.string,
getDonationButtonLabel: PropTypes.func.isRequired, getDonationButtonLabel: PropTypes.func.isRequired,
hideAmountOptionsCB: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
stripe: PropTypes.shape({ stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired createToken: PropTypes.func.isRequired
}) }),
theme: PropTypes.string
}; };
const initialState = { const initialState = {
donationState: { donationState: {
@ -38,7 +39,7 @@ const initialState = {
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userSelector, userSelector,
({ email }) => ({ email }) ({ email, theme }) => ({ email, theme })
); );
class DonateFormChildViewForHOC extends Component { class DonateFormChildViewForHOC extends Component {
@ -59,7 +60,6 @@ class DonateFormChildViewForHOC extends Component {
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.postDonation = this.postDonation.bind(this); this.postDonation = this.postDonation.bind(this);
this.resetDonation = this.resetDonation.bind(this); this.resetDonation = this.resetDonation.bind(this);
this.hideAmountOptions(false);
} }
getUserEmail() { getUserEmail() {
@ -113,11 +113,6 @@ class DonateFormChildViewForHOC extends Component {
}); });
} }
hideAmountOptions(hide) {
const { hideAmountOptionsCB } = this.props;
hideAmountOptionsCB(hide);
}
postDonation(token) { postDonation(token) {
const { donationAmount: amount, donationDuration: duration } = this.state; const { donationAmount: amount, donationDuration: duration } = this.state;
this.setState(state => ({ this.setState(state => ({
@ -128,10 +123,12 @@ class DonateFormChildViewForHOC extends Component {
} }
})); }));
// hide the donation options on the parent and scroll to top // scroll to top
this.hideAmountOptions(true);
window.scrollTo(0, 0); window.scrollTo(0, 0);
// change the donation modal button to close
this.props.changeCloseBtnLabel();
return postChargeStripe({ return postChargeStripe({
token, token,
amount, amount,
@ -179,7 +176,7 @@ class DonateFormChildViewForHOC extends Component {
renderDonateForm() { renderDonateForm() {
const { isFormValid } = this.state; const { isFormValid } = this.state;
const { getDonationButtonLabel } = this.props; const { getDonationButtonLabel, theme } = this.props;
return ( return (
<Form className='donation-form' onSubmit={this.handleSubmit}> <Form className='donation-form' onSubmit={this.handleSubmit}>
<FormGroup className='donation-email-container'> <FormGroup className='donation-email-container'>
@ -194,7 +191,10 @@ class DonateFormChildViewForHOC extends Component {
value={this.getUserEmail()} value={this.getUserEmail()}
/> />
</FormGroup> </FormGroup>
<StripeCardForm getValidationState={this.getValidationState} /> <StripeCardForm
getValidationState={this.getValidationState}
theme={theme}
/>
<Button <Button
block={true} block={true}
bsStyle='primary' bsStyle='primary'

View File

@ -4,18 +4,18 @@ import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Modal, Button } from '@freecodecamp/react-bootstrap'; import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
import { Link } from '../../../components/helpers'; import { Spacer } 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 Cup from '../../../assets/icons/Cup';
import MinimalDonateForm from './MinimalDonateForm';
import ga from '../../../analytics'; import ga from '../../../analytics';
import { import {
closeDonationModal, closeDonationModal,
isDonationModalOpenSelector, isDonationModalOpenSelector,
isBlockDonationModalSelector, isBlockDonationModalSelector
activeDonationsSelector
} from '../../../redux'; } from '../../../redux';
import { challengeMetaSelector } from '../../../templates/Challenges/redux'; import { challengeMetaSelector } from '../../../templates/Challenges/redux';
@ -26,12 +26,10 @@ const mapStateToProps = createSelector(
isDonationModalOpenSelector, isDonationModalOpenSelector,
challengeMetaSelector, challengeMetaSelector,
isBlockDonationModalSelector, isBlockDonationModalSelector,
activeDonationsSelector, (show, { block }, isBlockDonation) => ({
(show, { block }, isBlockDonation, activeDonors) => ({
show, show,
block, block,
isBlockDonation, isBlockDonation
activeDonors
}) })
); );
@ -51,13 +49,12 @@ const propTypes = {
show: PropTypes.bool show: PropTypes.bool
}; };
function DonateModal({ function DonateModal({ show, block, isBlockDonation, closeDonationModal }) {
show, const [showCloseLabel, setCloseLabel] = React.useState(false);
block, const changeCloseBtnLabel = () => {
activeDonors, setCloseLabel(true);
isBlockDonation, };
closeDonationModal
}) {
if (show) { if (show) {
ga.modalview('/donation-modal'); ga.modalview('/donation-modal');
} }
@ -66,12 +63,12 @@ function DonateModal({
<div className='donation-icon-container'> <div className='donation-icon-container'>
<Cup className='donation-icon' /> <Cup className='donation-icon' />
</div> </div>
<p className='text-center'> <Row>
Nicely done. You just completed {blockNameify(block)}. <Col sm={10} smOffset={1} xs={12}>
</p> <p>Nicely done. You just completed {blockNameify(block)}.</p>
<p className='text-center'> <p>Help us create even more learning resources like this.</p>
Help us create even more learning resources like this. </Col>
</p> </Row>
</div> </div>
); );
@ -80,14 +77,13 @@ function DonateModal({
<div className='donation-icon-container'> <div className='donation-icon-container'>
<Heart className='donation-icon' /> <Heart className='donation-icon' />
</div> </div>
<p> <Row>
freeCodeCamp.org is a tiny nonprofit that's helping millions of people <Col sm={10} smOffset={1} xs={12}>
learn to code for free. <p>
</p> Help us create even more learning resources for you and your family.
<p> </p>
Join <strong>{activeDonors}</strong> supporters. </Col>
</p> </Row>
<p>Your donation will help keep tech education free and open.</p>
</div> </div>
); );
@ -95,30 +91,28 @@ function DonateModal({
<Modal bsSize='lg' className='donation-modal' show={show}> <Modal bsSize='lg' className='donation-modal' show={show}>
<Modal.Header className='fcc-modal'> <Modal.Header className='fcc-modal'>
<Modal.Title className='modal-title text-center'> <Modal.Title className='modal-title text-center'>
<strong>Support freeCodeCamp.org</strong> <strong>Become a Supporter</strong>
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{isBlockDonation ? blockDonationText : progressDonationText} {isBlockDonation ? blockDonationText : progressDonationText}
<Spacer />
<MinimalDonateForm changeCloseBtnLabel={changeCloseBtnLabel} />
<Spacer />
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Button
block={true}
bsSize='sm'
bsStyle='primary'
className='btn-link'
onClick={closeDonationModal}
>
{showCloseLabel ? 'close' : 'Please ask me later.'}
</Button>
</Col>
</Row>
</Modal.Body> </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> </Modal>
); );
} }

View File

@ -0,0 +1,203 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Row, Col } from '@freecodecamp/react-bootstrap';
import { StripeProvider, Elements } from 'react-stripe-elements';
import {
amountsConfig,
durationsConfig,
defaultStateConfig
} from '../../../../../config/donation-settings';
import { apiLocation } from '../../../../config/env.json';
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
import {
userSelector,
isSignedInSelector,
signInLoadingSelector,
hardGoTo as navigate
} from '../../../redux';
import '../Donation.css';
import DonateCompletion from './DonateCompletion.js';
import { stripePublicKey } from '../../../../../config/env.json';
import { stripeScriptLoader } from '../../../utils/scriptLoaders';
const numToCommas = num =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = {
changeCloseBtnLabel: PropTypes.func,
isDonating: PropTypes.bool,
isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired,
stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired
})
};
const mapStateToProps = createSelector(
userSelector,
signInLoadingSelector,
isSignedInSelector,
({ isDonating }, showLoading, isSignedIn) => ({
isDonating,
isSignedIn,
showLoading
})
);
const mapDispatchToProps = {
navigate
};
const createOnClick = navigate => e => {
e.preventDefault();
return navigate(`${apiLocation}/signin?returnTo=donate`);
};
class ModalDonateForm extends Component {
constructor(...args) {
super(...args);
this.durations = durationsConfig;
this.amounts = amountsConfig;
this.state = {
...defaultStateConfig,
isDonating: this.props.isDonating,
stripe: null
};
this.handleSelectPaymentType = this.handleSelectPaymentType.bind(this);
this.handleStripeLoad = this.handleStripeLoad.bind(this);
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
}
componentDidMount() {
if (window.Stripe) {
this.handleStripeLoad();
} else if (document.querySelector('#stripe-js')) {
document
.querySelector('#stripe-js')
.addEventListener('load', this.handleStripeLoad);
} else {
stripeScriptLoader(this.handleStripeLoad);
}
}
componentWillUnmount() {
const stripeMountPoint = document.querySelector('#stripe-js');
if (stripeMountPoint) {
stripeMountPoint.removeEventListener('load', this.handleStripeLoad);
}
}
handleStripeLoad() {
// Create Stripe instance once Stripe.js loads
if (stripePublicKey) {
this.setState(state => ({
...state,
stripe: window.Stripe(stripePublicKey)
}));
}
}
handleSelectPaymentType(e) {
this.setState({
paymentType: e.currentTarget.value
});
}
getFormatedAmountLabel(amount) {
return `$${numToCommas(amount / 100)}`;
}
getDonationButtonLabel() {
const { donationAmount, donationDuration } = this.state;
let donationBtnLabel = `Confirm your donation`;
if (donationDuration === 'onetime') {
donationBtnLabel = `Confirm your one-time donation of ${this.getFormatedAmountLabel(
donationAmount
)}`;
} else {
donationBtnLabel = `Confirm your donation of ${this.getFormatedAmountLabel(
donationAmount
)} ${donationDuration === 'month' ? 'per month' : 'per year'}`;
}
return donationBtnLabel;
}
renderDonationOptions() {
const {
donationAmount,
donationDuration,
paymentType,
stripe
} = this.state;
const { changeCloseBtnLabel } = this.props;
return (
<div>
{paymentType === 'Card' ? (
<StripeProvider stripe={stripe}>
<Elements>
<DonateFormChildViewForHOC
changeCloseBtnLabel={changeCloseBtnLabel}
donationAmount={donationAmount}
donationDuration={donationDuration}
getDonationButtonLabel={this.getDonationButtonLabel}
/>
</Elements>
</StripeProvider>
) : (
<p>
PayPal is currently unavailable. Please use a Credit/Debit card
instead.
</p>
)}
</div>
);
}
render() {
const { isSignedIn, navigate, showLoading, isDonating } = this.props;
if (isDonating) {
return (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<DonateCompletion success={true} />
</Col>
</Row>
);
}
return (
<Row>
<Col sm={10} smOffset={1} xs={12}>
{!showLoading && !isSignedIn ? (
<Button
bsStyle='default'
className='btn btn-block'
onClick={createOnClick(navigate)}
>
Become a supporter
</Button>
) : (
this.renderDonationOptions()
)}
</Col>
</Row>
);
}
}
ModalDonateForm.displayName = 'ModalDonateForm';
ModalDonateForm.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ModalDonateForm);

View File

@ -1,10 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { CardNumberElement, CardExpiryElement } from 'react-stripe-elements';
CardNumberElement,
CardExpiryElement,
CardCVCElement
} from 'react-stripe-elements';
import { ControlLabel, FormGroup } from '@freecodecamp/react-bootstrap'; import { ControlLabel, FormGroup } from '@freecodecamp/react-bootstrap';
const propTypes = { const propTypes = {
@ -31,10 +27,6 @@ class StripeCardForm extends Component {
cardExpiry: { cardExpiry: {
complete: false, complete: false,
error: null error: null
},
cardCvc: {
complete: false,
error: null
} }
} }
}; };
@ -92,14 +84,6 @@ class StripeCardForm extends Component {
style={style} style={style}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<ControlLabel>Your Card CVC (3-digit security number):</ControlLabel>
<CardCVCElement
className='form-control donate-input-element'
onChange={this.handleInputChange}
style={style}
/>
</FormGroup>
</div> </div>
); );
} }

View File

@ -11,7 +11,7 @@ const amountsConfig = {
}; };
const defaultAmount = { const defaultAmount = {
year: 25000, year: 25000,
month: 3500, month: 500,
onetime: 25000 onetime: 25000
}; };
const defaultStateConfig = { const defaultStateConfig = {