From 4cd55424897f9bf3c02febbe5e573a029fd15802 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Tue, 5 Nov 2019 19:23:36 +0530 Subject: [PATCH] feat(donate): two col layout checkout page --- client/src/components/Donation/Donation.css | 1 - .../Donation/components/DonateCompletion.js | 13 +- .../Donation/components/DonateForm.js | 267 +++++------------- .../components/DonateFormChildViewForHOC.js | 224 +++++++++++++++ .../Donation/components/DonateOther.js | 261 ----------------- .../Donation/components/DonateText.js | 55 ++-- .../Donation/components/StripeCardForm.js | 2 - client/src/pages/donate.js | 77 ++--- 8 files changed, 353 insertions(+), 547 deletions(-) create mode 100644 client/src/components/Donation/components/DonateFormChildViewForHOC.js delete mode 100644 client/src/components/Donation/components/DonateOther.js diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css index 47f2807bfd..9941ffd1a5 100644 --- a/client/src/components/Donation/Donation.css +++ b/client/src/components/Donation/Donation.css @@ -19,7 +19,6 @@ .donation-form { display: flex; flex-direction: column; - width: 80%; justify-content: center; margin: 0 auto; } diff --git a/client/src/components/Donation/components/DonateCompletion.js b/client/src/components/Donation/components/DonateCompletion.js index e2c35f2041..ef3d49418e 100644 --- a/client/src/components/Donation/components/DonateCompletion.js +++ b/client/src/components/Donation/components/DonateCompletion.js @@ -19,7 +19,7 @@ function DonateCompletion({ processing, reset, success, error = null }) { ? 'We are processing your donation.' : success ? 'Your donation was successful.' - : 'Something went wrong with your donation'; + : 'Something went wrong with your donation.'; return (

@@ -35,7 +35,16 @@ function DonateCompletion({ processing, reset, success, error = null }) { /> )} {success && ( -

Thank you for supporting the freeCodeCamp.org community.

+
+

+ You can update your supporter status at any time from your account + settings. +

+

+ Thank you for supporting free technology education for people all + over the world. +

+
)} {error &&

{error}

} diff --git a/client/src/components/Donation/components/DonateForm.js b/client/src/components/Donation/components/DonateForm.js index 7f8539512a..09242ca055 100644 --- a/client/src/components/Donation/components/DonateForm.js +++ b/client/src/components/Donation/components/DonateForm.js @@ -2,227 +2,116 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import isEmail from 'validator/lib/isEmail'; +import { Button } from '@freecodecamp/react-bootstrap'; +import { StripeProvider, Elements } from 'react-stripe-elements'; +import { apiLocation } from '../../../../config/env.json'; +import DonateFormChildViewForHOC from './DonateFormChildViewForHOC'; import { - Button, - ControlLabel, - Form, - FormControl, - FormGroup, - Row, - Col -} from '@freecodecamp/react-bootstrap'; -import { injectStripe } from 'react-stripe-elements'; + userSelector, + isSignedInSelector, + signInLoadingSelector, + hardGoTo as navigate +} from '../../../redux'; -import Spacer from '../../../components/helpers/Spacer'; -import StripeCardForm from './StripeCardForm'; -import DonateCompletion from './DonateCompletion'; -import { postChargeStripe } from '../../../utils/ajax'; -import { userSelector, isSignedInSelector } from '../../../redux'; +import '../Donation.css'; const propTypes = { - email: PropTypes.string, isSignedIn: PropTypes.bool, + navigate: PropTypes.func.isRequired, + showLoading: PropTypes.bool.isRequired, stripe: PropTypes.shape({ createToken: PropTypes.func.isRequired - }), - theme: PropTypes.string -}; -const initialState = { - donationAmount: 500, - donationState: { - processing: false, - success: false, - error: '' - } + }) }; const mapStateToProps = createSelector( userSelector, + signInLoadingSelector, isSignedInSelector, - ({ email, theme }, isSignedIn) => ({ email, theme, isSignedIn }) + ({ email, theme }, showLoading, isSignedIn) => ({ + email, + theme, + showLoading, + isSignedIn + }) ); +const mapDispatchToProps = { + navigate +}; + +const createOnClick = navigate => e => { + e.preventDefault(); + return navigate(`${apiLocation}/signin?returnTo=donate`); +}; class DonateForm extends Component { constructor(...args) { super(...args); this.state = { - ...initialState, - email: null, - isFormValid: false + donationAmount: 500 }; - this.getUserEmail = this.getUserEmail.bind(this); - this.getValidationState = this.getValidationState.bind(this); - this.handleEmailChange = this.handleEmailChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.postDonation = this.postDonation.bind(this); - this.resetDonation = this.resetDonation.bind(this); + this.buttonSingleAmounts = [2500, 5000, 10000, 25000]; + this.buttonMonthlyAmounts = [500, 1000, 2000]; + this.buttonAnnualAmounts = [6000, 10000, 25000, 50000]; + + this.isActive = this.isActive.bind(this); + this.renderAmountButtons = this.renderAmountButtons.bind(this); } - getUserEmail() { - const { email: stateEmail } = this.state; - const { email: propsEmail } = this.props; - return stateEmail || propsEmail || ''; + isActive(amount) { + return this.state.donationAmount === amount; } - getValidationState(isFormValid) { - this.setState(state => ({ - ...state, - isFormValid - })); - } - - handleEmailChange(e) { - const newValue = e.target.value; - return this.setState(state => ({ - ...state, - email: newValue - })); - } - - handleSubmit(e) { - e.preventDefault(); - const email = this.getUserEmail(); - if (!email || !isEmail(email)) { - return this.setState(state => ({ - ...state, - donationState: { - ...state.donationState, - error: - 'We need a valid email address to which we can send your' + - ' donation tax receipt.' - } - })); - } - return this.props.stripe.createToken({ email }).then(({ error, token }) => { - if (error) { - return this.setState(state => ({ - ...state, - donationState: { - ...state.donationState, - error: - 'Something went wrong processing your donation. Your card' + - ' has not been charged.' - } - })); - } - return this.postDonation(token); - }); - } - - postDonation(token) { - const { donationAmount: amount } = this.state; - const { isSignedIn } = this.props; - this.setState(state => ({ - ...state, - donationState: { - ...state.donationState, - processing: true - } - })); - - return postChargeStripe(isSignedIn, { - token, - amount - }) - .then(response => { - const data = response && response.data; - this.setState(state => ({ - ...state, - donationState: { - ...state.donationState, - processing: false, - success: true, - error: data.error ? data.error : null - } - })); - }) - .catch(error => { - const data = - error.response && error.response.data - ? error.response.data - : { - error: - 'Something is not right. Please contact team@freecodecamp.org' - }; - this.setState(state => ({ - ...state, - donationState: { - ...state.donationState, - processing: false, - success: false, - error: data.error - } - })); - }); - } - - resetDonation() { - return this.setState({ ...initialState }); - } - - renderCompletion(props) { - return ; - } - - renderDonateForm() { - const { isFormValid } = this.state; - const { theme } = this.props; - return ( - - -
- - - Email (we'll send you a tax-deductible donation receipt): - - - - - - - - -
- ); + renderAmountButtons() { + return this.buttonAnnualAmounts.map(amount => ( + + )); } render() { - const { - donationState: { processing, success, error } - } = this.state; - if (processing || success || error) { - return this.renderCompletion({ - processing, - success, - error, - reset: this.resetDonation - }); + const { isSignedIn, navigate, showLoading, stripe } = this.props; + + if (!showLoading && !isSignedIn) { + return ( +
+ +
+ ); } - return this.renderDonateForm(); + + return ( +
+ + + + + +
+ ); } } DonateForm.displayName = 'DonateForm'; DonateForm.propTypes = propTypes; -export default injectStripe(connect(mapStateToProps)(DonateForm)); +export default connect( + mapStateToProps, + mapDispatchToProps +)(DonateForm); diff --git a/client/src/components/Donation/components/DonateFormChildViewForHOC.js b/client/src/components/Donation/components/DonateFormChildViewForHOC.js new file mode 100644 index 0000000000..b6b2f53cb1 --- /dev/null +++ b/client/src/components/Donation/components/DonateFormChildViewForHOC.js @@ -0,0 +1,224 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import isEmail from 'validator/lib/isEmail'; +import { + Button, + ControlLabel, + Form, + FormControl, + FormGroup +} from '@freecodecamp/react-bootstrap'; +import { injectStripe } from 'react-stripe-elements'; + +import Spacer from '../../helpers/Spacer'; +import StripeCardForm from './StripeCardForm'; +import DonateCompletion from './DonateCompletion'; +import { postChargeStripe } from '../../../utils/ajax'; +import { userSelector, isSignedInSelector } from '../../../redux'; + +const propTypes = { + email: PropTypes.string, + isSignedIn: PropTypes.bool, + stripe: PropTypes.shape({ + createToken: PropTypes.func.isRequired + }), + theme: PropTypes.string +}; +const initialState = { + donationAmount: 500, + donationState: { + processing: false, + success: false, + error: '' + } +}; + +const mapStateToProps = createSelector( + userSelector, + isSignedInSelector, + ({ email, theme }, isSignedIn) => ({ email, theme, isSignedIn }) +); + +class DonateFormChildViewForHOC extends Component { + constructor(...args) { + super(...args); + + this.state = { + ...initialState, + email: null, + isFormValid: false + }; + + this.getUserEmail = this.getUserEmail.bind(this); + this.getValidationState = this.getValidationState.bind(this); + this.handleEmailChange = this.handleEmailChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.postDonation = this.postDonation.bind(this); + this.resetDonation = this.resetDonation.bind(this); + } + + getUserEmail() { + const { email: stateEmail } = this.state; + const { email: propsEmail } = this.props; + return stateEmail || propsEmail || ''; + } + + getValidationState(isFormValid) { + this.setState(state => ({ + ...state, + isFormValid + })); + } + + handleEmailChange(e) { + const newValue = e.target.value; + return this.setState(state => ({ + ...state, + email: newValue + })); + } + + handleSubmit(e) { + e.preventDefault(); + const email = this.getUserEmail(); + if (!email || !isEmail(email)) { + return this.setState(state => ({ + ...state, + donationState: { + ...state.donationState, + error: + 'We need a valid email address to which we can send your' + + ' donation tax receipt.' + } + })); + } + return this.props.stripe.createToken({ email }).then(({ error, token }) => { + if (error) { + return this.setState(state => ({ + ...state, + donationState: { + ...state.donationState, + error: + 'Something went wrong processing your donation. Your card' + + ' has not been charged.' + } + })); + } + return this.postDonation(token); + }); + } + + postDonation(token) { + const { donationAmount: amount } = this.state; + const { isSignedIn } = this.props; + this.setState(state => ({ + ...state, + donationState: { + ...state.donationState, + processing: true + } + })); + + return postChargeStripe(isSignedIn, { + token, + amount + }) + .then(response => { + const data = response && response.data; + this.setState(state => ({ + ...state, + donationState: { + ...state.donationState, + processing: false, + success: true, + error: data.error ? data.error : null + } + })); + }) + .catch(error => { + const data = + error.response && error.response.data + ? error.response.data + : { + error: + 'Something is not right. Please contact team@freecodecamp.org' + }; + this.setState(state => ({ + ...state, + donationState: { + ...state.donationState, + processing: false, + success: false, + error: data.error + } + })); + }); + } + + resetDonation() { + return this.setState({ ...initialState }); + } + + renderCompletion(props) { + return ; + } + + renderDonateForm() { + const { isFormValid } = this.state; + const { theme } = this.props; + return ( +
+ + + Email (we'll send you a tax-deductible donation receipt): + + + + + + + + ); + } + + render() { + const { + donationState: { processing, success, error } + } = this.state; + if (processing || success || error) { + return this.renderCompletion({ + processing, + success, + error, + reset: this.resetDonation + }); + } + return this.renderDonateForm(); + } +} + +DonateFormChildViewForHOC.displayName = 'DonateFormChildViewForHOC'; +DonateFormChildViewForHOC.propTypes = propTypes; + +export default injectStripe( + connect(mapStateToProps)(DonateFormChildViewForHOC) +); diff --git a/client/src/components/Donation/components/DonateOther.js b/client/src/components/Donation/components/DonateOther.js deleted file mode 100644 index fd120992dc..0000000000 --- a/client/src/components/Donation/components/DonateOther.js +++ /dev/null @@ -1,261 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { Grid, Col, Row } from '@freecodecamp/react-bootstrap'; - -import ReactGA from '../../../analytics/index.js'; -import { Link, Spacer } from '../../helpers'; - -const paypalMonthlyDonations = [ - { - eventLabel: 'paypal', - eventValue: 5, - defaultValueHash: 'KTAXU8B4MYAT8', - defaultValue: 'Donate $5 each month' - }, - { - eventLabel: 'paypal', - eventValue: 10, - defaultValueHash: 'BYEBQEHS5LHNC', - defaultValue: 'Donate $10 each month' - }, - { - eventLabel: 'paypal', - eventValue: 35, - defaultValueHash: '57ZPMKJ8G893Y', - defaultValue: 'Donate $35 each month' - }, - { - eventLabel: 'paypal', - eventValue: 50, - defaultValueHash: '2ZVYTHK6Q7AFW', - defaultValue: 'Donate $50 each month' - }, - { - eventLabel: 'paypal', - eventValue: 100, - defaultValueHash: 'C7PUT3LMJHKK2', - defaultValue: 'Donate $100 each month' - }, - { - eventLabel: 'paypal', - eventValue: 250, - defaultValueHash: '69JGTY4DHSTEN', - defaultValue: 'Donate $250 each month' - } -]; - -const paypalOneTimeDonation = { - eventLabel: 'paypal one time donation', - eventValue: 0, - defaultValueHash: 'B256JC6ZCWD3J', - defaultValue: 'Make a one-time donation' -}; - -class DonateOther extends Component { - renderForm(item) { - return ( -
- ReactGA.event({ - category: 'donation', - action: 'click', - label: item.eventLabel, - value: item.eventValue - }) - } - target='_blank' - > - {' '} - {' '} - -
- ); - } - - /* eslint-disable max-len */ - render() { - return ( - - - - - -

- Other ways you can support our nonprofit -

-

- freeCodeCamp is a small donor-supported 501(c)(3) public - charity. We are tax-exempt, so you can deduct donations you make - to our nonprofit from your taxes. You can{' '} - - download our IRS Determination Letter here - - . -

-
-

Set up a monthly donation using PayPal

-

- You can set up a monthly donation to freeCodeCamp by clicking - one of the links below and following the instructions on PayPal. - You can easily stop your donations at any time in the future. -

- - - {paypalMonthlyDonations.map(item => { - return this.renderForm(item); - })} - - -
-

Make a one-time donation using PayPal

-

- You can make a one-time monthly donation to freeCodeCamp for any - amount of money by clicking one of the links below and following - the instructions on PayPal: -

- - - {this.renderForm(paypalOneTimeDonation)} - - - -
-

Get your employer to match your donation

-

- Many freeCodeCamp supporters are able to get their employers to - match their donations to freeCodeCamp. Our Tax-exempt number - (EIN) is 82-0779546. If we can help you with setting this up, - please email{' '} - team@freecodecamp.org - . -

-
-

Donate through a payroll deduction

-

- In the US and Canada, some employers have a convenient way to - give to freeCodeCamp through a payroll deduction. Ask your - employer if they can do this, and have them send any necessary - paperwork to: -

-

- Free Code Camp, Inc. -
- 7800 W Hefner Rd, PO BOX 721024 -
- Oklahoma City, Oklahoma 73172 -
- United States -

-
-

Donate cryptocurrency to freeCodeCamp

-

- Below are our wallets where we can receive cryptocurrency - donations. -

-

Make a one-time Bitcoin donation

-

- Our Bitcoin wallet is{' '} - - 12skYi7aMCjDUdrVdoB3JjZ77ug8gxJfbL - -

-

Make a one-time Ethereum donation

-

- Our Ethereum wallet is{' '} - - 0x0ADbEf2471416BD8732cf0f3944294eE393CcAF5 - -

-

Make a one-time Litecoin donation

-

- Our Litecoin wallet is{' '} - - LKu8UG8Z1nbTxnq9Do96PsC3FwbNtycf3X - -

-

Make a one-time Bitcoin Cash donation

-

- Our Bitcoin Cash wallet is{' '} - - 1EBxPEJWrGZWxe2UayyAsnd5VsRg5H9xfu - -

-
-

Donate to freeCodeCamp by mailing us a check

-

Our mailing address is:

-

- Free Code Camp, Inc. -
- 7800 W Hefner Rd, PO BOX 721024 -
- Oklahoma City, Oklahoma 73172 -
- United States -

-
-

Donate Stock to freeCodeCamp

-

- If you want to donate stocks to freeCodeCamp, please email us at{' '} - team@freecodecamp.org - . -

-
-

Legacy Gift

-

- You can help future generations of learners by listing - freeCodeCamp in your will or living trust. If you're interested - in doing this, email Quincy directly and we can discuss this:{' '} - - quincy@freecodecamp.org - - . -

-
-

- Thank you for supporting our nonprofit and the freeCodeCamp - community. -

- -
- Donate using a Credit or Debit card -
- - -
-
-
- ); - } - /* eslint-enable max-len */ -} - -DonateOther.displayName = 'DonateOther'; - -export default DonateOther; diff --git a/client/src/components/Donation/components/DonateText.js b/client/src/components/Donation/components/DonateText.js index a4c23990e1..a67825f96b 100644 --- a/client/src/components/Donation/components/DonateText.js +++ b/client/src/components/Donation/components/DonateText.js @@ -1,41 +1,30 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { Row, Col } from '@freecodecamp/react-bootstrap'; -import { activeDonationsSelector } from '../../../redux'; - -const propTypes = { - activeDonations: PropTypes.number -}; - -const mapStateToProps = createSelector( - activeDonationsSelector, - activeDonations => ({ activeDonations }) -); - -const DonateText = ({ activeDonations }) => { - const donationsLocale = activeDonations.toLocaleString(); +const DonateText = () => { return ( - - -

- freeCodeCamp.org is a tiny nonprofit that's helping millions of people - learn to code for free. -

-

- Join {donationsLocale} supporters. -

-

- Your $5 / month donation will help keep tech education free and open. -

- -
+ <> +

freeCodeCamp is a highly efficient education nonprofit.

+

+ In 2019 alone, we provided 1,100,000,000 minutes of free education to + people around the world. +

+

+ Since freeCodeCamp's total budget is only $373,000, that means every + dollar you donate to freeCodeCamp translates into 50 hours worth of + technology education. +

+

+ When you donate to freeCodeCamp, you help people learn new skills and + provide for their families. +

+

+ You also help us create new resources for you to use to expand your own + technology skills. +

+ ); }; DonateText.displayName = 'DonateText'; -DonateText.propTypes = propTypes; -export default connect(mapStateToProps)(DonateText); +export default DonateText; diff --git a/client/src/components/Donation/components/StripeCardForm.js b/client/src/components/Donation/components/StripeCardForm.js index 086c14b901..0f1c1dbd3b 100644 --- a/client/src/components/Donation/components/StripeCardForm.js +++ b/client/src/components/Donation/components/StripeCardForm.js @@ -7,8 +7,6 @@ import { } from 'react-stripe-elements'; import { ControlLabel, FormGroup } from '@freecodecamp/react-bootstrap'; -import '../Donation.css'; - const propTypes = { getValidationState: PropTypes.func.isRequired, theme: PropTypes.string diff --git a/client/src/pages/donate.js b/client/src/pages/donate.js index 9427478e26..2471f39d41 100644 --- a/client/src/pages/donate.js +++ b/client/src/pages/donate.js @@ -1,40 +1,25 @@ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; -import { StripeProvider, Elements } from 'react-stripe-elements'; -import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap'; +import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import { Grid, Row, Col } from '@freecodecamp/react-bootstrap'; -import { stripePublicKey, apiLocation } from '../../config/env.json'; +import { stripePublicKey } from '../../config/env.json'; import { Spacer, Loader } from '../components/helpers'; -import DonateOther from '../components/Donation/components/DonateOther'; import DonateForm from '../components/Donation/components/DonateForm'; import DonateText from '../components/Donation/components/DonateText'; -import PoweredByStripe from '../components/Donation/components/poweredByStripe'; -import { - signInLoadingSelector, - isSignedInSelector, - hardGoTo as navigate -} from '../redux'; +import { signInLoadingSelector } from '../redux'; import { stripeScriptLoader } from '../utils/scriptLoaders'; const mapStateToProps = createSelector( signInLoadingSelector, - isSignedInSelector, - (showLoading, isSignedIn) => ({ - showLoading, - isSignedIn + showLoading => ({ + showLoading }) ); -const mapDispatchToProps = { - navigate -}; - const propTypes = { - isSignedIn: PropTypes.bool.isRequired, - navigate: PropTypes.func.isRequired, showLoading: PropTypes.bool.isRequired }; @@ -42,11 +27,9 @@ export class DonatePage extends Component { constructor(...props) { super(...props); this.state = { - stripe: null, - showOtherOptions: false + stripe: null }; this.handleStripeLoad = this.handleStripeLoad.bind(this); - this.toggleOtherOptions = this.toggleOtherOptions.bind(this); } componentDidMount() { @@ -77,54 +60,33 @@ export class DonatePage extends Component { })); } - toggleOtherOptions() { - this.setState(({ showOtherOptions }) => ({ - showOtherOptions: !showOtherOptions - })); - } - render() { - const { showOtherOptions, stripe } = this.state; - const { showLoading, isSignedIn, navigate } = this.props; + const { stripe } = this.state; + const { showLoading } = this.props; if (showLoading) { return ; } - if (!showLoading && !isSignedIn) { - navigate(`${apiLocation}/signin?returnTo=donate`); - return ; - } - return ( -

Become a Supporter

- - - -
- - - - - -
- - - -
+ + + + + + + +
- {showOtherOptions && }
); } @@ -133,7 +95,4 @@ export class DonatePage extends Component { DonatePage.displayName = 'DonatePage'; DonatePage.propTypes = propTypes; -export default connect( - mapStateToProps, - mapDispatchToProps -)(DonatePage); +export default connect(mapStateToProps)(DonatePage);