From e54e2588bfcbf4bb0c40111bb73c17c378f1068e Mon Sep 17 00:00:00 2001 From: awu43 <46470763+awu43@users.noreply.github.com> Date: Fri, 25 Jun 2021 08:32:57 -0700 Subject: [PATCH] feat(client): ts-migrate client/src/pages (#42445) * Renamed .js files to .tsx * Migrated 404 to TS TODO: Ask about default prop * Migrated certification to TS Converted to functional component * Partially migrated challenges to TS TODO: Ask about Redirect props * Installed @types/react-helmet and @types/react-redux Prep for migrating donate, this caused two new TS errors on 404 and certification * Migrated donate to TS TODO: Ask about prop spreading * Migrated email-sign-up to TS Converted to functional component and removed unused isSignedIn prop * Migrated index to TS Removed unused props * Migrated learn to TS * Installed @types/react-instantsearch-dom Prep for migrating search * Migrated search to TS Converted to functional component * Migrated settings to TS * Migrated unsubscribed to TS * Installed @types/validator and @types/lodash-es Prep for migrating update-email * Migrated update-email to TS Converted to functional component * Migrated user to TS * Updated effect hook dependencies Also removed unnecessary comments from 404 * Renamed challenges.test.tsx to .ts * remove search.tsx, as search.js was removed * revert: packages * revert: packages Co-authored-by: Shaun Hamilton Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com> --- client/src/pages/{404.js => 404.tsx} | 7 +- client/src/pages/certification.js | 23 --- client/src/pages/certification.tsx | 26 ++++ client/src/pages/challenges.js | 26 ---- ...{challenges.test.js => challenges.test.ts} | 0 client/src/pages/challenges.tsx | 44 ++++++ client/src/pages/donate.js | 140 ----------------- client/src/pages/donate.tsx | 142 +++++++++++++++++ client/src/pages/email-sign-up.js | 114 -------------- client/src/pages/email-sign-up.tsx | 117 ++++++++++++++ client/src/pages/index.js | 20 --- client/src/pages/index.tsx | 11 ++ client/src/pages/{learn.js => learn.tsx} | 57 ++++--- .../src/pages/{settings.js => settings.tsx} | 4 +- .../{unsubscribed.js => unsubscribed.tsx} | 8 +- client/src/pages/update-email.js | 144 ------------------ client/src/pages/update-email.tsx | 132 ++++++++++++++++ client/src/pages/{user.js => user.tsx} | 4 +- 18 files changed, 524 insertions(+), 495 deletions(-) rename client/src/pages/{404.js => 404.tsx} (62%) delete mode 100644 client/src/pages/certification.js create mode 100644 client/src/pages/certification.tsx delete mode 100644 client/src/pages/challenges.js rename client/src/pages/{challenges.test.js => challenges.test.ts} (100%) create mode 100644 client/src/pages/challenges.tsx delete mode 100644 client/src/pages/donate.js create mode 100644 client/src/pages/donate.tsx delete mode 100644 client/src/pages/email-sign-up.js create mode 100644 client/src/pages/email-sign-up.tsx delete mode 100644 client/src/pages/index.js create mode 100644 client/src/pages/index.tsx rename client/src/pages/{learn.js => learn.tsx} (73%) rename client/src/pages/{settings.js => settings.tsx} (76%) rename client/src/pages/{unsubscribed.js => unsubscribed.tsx} (61%) delete mode 100644 client/src/pages/update-email.js create mode 100644 client/src/pages/update-email.tsx rename client/src/pages/{user.js => user.tsx} (76%) diff --git a/client/src/pages/404.js b/client/src/pages/404.tsx similarity index 62% rename from client/src/pages/404.js rename to client/src/pages/404.tsx index d804c3a98a..95ff28fefe 100644 --- a/client/src/pages/404.js +++ b/client/src/pages/404.tsx @@ -7,10 +7,15 @@ import FourOhFour from '../components/FourOhFour'; import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four'; /* eslint-enable max-len */ -function FourOhFourPage() { +function FourOhFourPage(): JSX.Element { return ( + {/* Error from installing @types/react-helmet and @types/react-redux */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} ); diff --git a/client/src/pages/certification.js b/client/src/pages/certification.js deleted file mode 100644 index 1cfbf09371..0000000000 --- a/client/src/pages/certification.js +++ /dev/null @@ -1,23 +0,0 @@ -import React, { Component } from 'react'; -import { Router } from '@reach/router'; -import { withPrefix } from 'gatsby'; - -import RedirectHome from '../components/RedirectHome'; -import ShowCertification from '../client-only-routes/show-certification'; - -import './certification.css'; - -class Certification extends Component { - render() { - return ( - - - - - ); - } -} - -export default Certification; diff --git a/client/src/pages/certification.tsx b/client/src/pages/certification.tsx new file mode 100644 index 0000000000..d357566327 --- /dev/null +++ b/client/src/pages/certification.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Router } from '@reach/router'; +import { withPrefix } from 'gatsby'; + +import RedirectHome from '../components/RedirectHome'; +import ShowCertification from '../client-only-routes/show-certification'; + +import './certification.css'; + +function Certification(): JSX.Element { + return ( + + + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + + ); +} + +export default Certification; diff --git a/client/src/pages/challenges.js b/client/src/pages/challenges.js deleted file mode 100644 index 0d263bdb34..0000000000 --- a/client/src/pages/challenges.js +++ /dev/null @@ -1,26 +0,0 @@ -// this exists purely to redirect legacy challenge paths to /learn -import React from 'react'; -import { Router } from '@reach/router'; -import { navigate, withPrefix } from 'gatsby'; - -import toLearnPath from '../utils/to-learn-path'; - -const Redirect = props => { - if (typeof window !== 'undefined') { - navigate(toLearnPath(props)); - } - return null; -}; - -const Challenges = () => ( - - - - - - -); - -Challenges.displayName = 'Challenges'; - -export default Challenges; diff --git a/client/src/pages/challenges.test.js b/client/src/pages/challenges.test.ts similarity index 100% rename from client/src/pages/challenges.test.js rename to client/src/pages/challenges.test.ts diff --git a/client/src/pages/challenges.tsx b/client/src/pages/challenges.tsx new file mode 100644 index 0000000000..b85e576c65 --- /dev/null +++ b/client/src/pages/challenges.tsx @@ -0,0 +1,44 @@ +// this exists purely to redirect legacy challenge paths to /learn +import React from 'react'; +import { Router } from '@reach/router'; +import { navigate, withPrefix } from 'gatsby'; + +import toLearnPath from '../utils/to-learn-path'; + +// interface RedirectProps1 { +// superBlock: string; +// block: string; +// challenge: string; +// } + +// interface RedirectProps2 { +// path?: string; +// default?: boolean; +// } + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +function Redirect(props) { + if (typeof window !== 'undefined') { + void navigate(toLearnPath(props)); + } + return null; +} +// Unsure about Redirect props shape: +// toLearnPath() takes required superBlock, block, and challenge props +// but usage below has optional path and default props + +function Challenges(): JSX.Element { + return ( + + + + + + + ); +} + +Challenges.displayName = 'Challenges'; + +export default Challenges; diff --git a/client/src/pages/donate.js b/client/src/pages/donate.js deleted file mode 100644 index 2263d22fe3..0000000000 --- a/client/src/pages/donate.js +++ /dev/null @@ -1,140 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import Helmet from 'react-helmet'; -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap'; -import { withTranslation } from 'react-i18next'; - -import { Spacer, Loader } from '../components/helpers'; -import DonateForm from '../components/Donation/DonateForm'; -import { - DonationText, - DonationSupportText, - DonationOptionsText, - DonationOptionsAlertText -} from '../components/Donation/DonationTextComponents'; -import { signInLoadingSelector, userSelector, executeGA } from '../redux'; -import CampersImage from '../components/landing/components/CampersImage'; - -const propTypes = { - executeGA: PropTypes.func, - isDonating: PropTypes.bool, - showLoading: PropTypes.bool.isRequired, - t: PropTypes.func.isRequired -}; - -const mapStateToProps = createSelector( - userSelector, - signInLoadingSelector, - ({ isDonating }, showLoading) => ({ - isDonating, - showLoading - }) -); - -const mapDispatchToProps = dispatch => - bindActionCreators( - { - executeGA - }, - dispatch - ); - -class DonatePage extends Component { - constructor(...props) { - super(...props); - this.state = { - enableSettings: false - }; - this.handleProcessing = this.handleProcessing.bind(this); - } - - componentDidMount() { - this.props.executeGA({ - type: 'event', - data: { - category: 'Donation View', - action: `Displayed donate page`, - nonInteraction: true - } - }); - } - - handleProcessing(duration, amount, action) { - this.props.executeGA({ - type: 'event', - data: { - category: 'Donation', - action: `donate page ${action}`, - label: duration, - value: amount - } - }); - } - - render() { - const { showLoading, isDonating, t } = this.props; - - if (showLoading) { - return ; - } - - return ( - - - - - - - - - - {isDonating ? ( -

{t('donate.thank-you')}

- ) : ( -

{t('donate.help-more')}

- )} - - -
- {isDonating ? ( - -

{t('donate.thank-you-2')}

-
- -
- ) : null} - - - - -
- - - -
- - - - -
-
- -
-
- ); - } -} - -DonatePage.displayName = 'DonatePage'; -DonatePage.propTypes = propTypes; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withTranslation()(DonatePage)); diff --git a/client/src/pages/donate.tsx b/client/src/pages/donate.tsx new file mode 100644 index 0000000000..ffb1b367c2 --- /dev/null +++ b/client/src/pages/donate.tsx @@ -0,0 +1,142 @@ +import React, { useEffect } from 'react'; +import Helmet from 'react-helmet'; +import { bindActionCreators } from 'redux'; +import type { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap'; +import { withTranslation } from 'react-i18next'; + +import { Spacer, Loader } from '../components/helpers'; +import DonateForm from '../components/Donation/DonateForm'; +import { + DonationText, + DonationSupportText, + DonationOptionsText, + DonationOptionsAlertText +} from '../components/Donation/DonationTextComponents'; +import { signInLoadingSelector, userSelector, executeGA } from '../redux'; +import CampersImage from '../components/landing/components/CampersImage'; + +interface ExecuteGaArg { + type: string; + data: { + category: string; + action: string; + nonInteraction?: boolean; + label?: string; + value?: number; + }; +} +interface DonatePageProps { + executeGA: (arg: ExecuteGaArg) => void; + isDonating?: boolean; + showLoading: boolean; + t: (s: string) => string; +} + +const mapStateToProps = createSelector( + userSelector, + signInLoadingSelector, + ({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({ + isDonating, + showLoading + }) +); + +const mapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators({ executeGA }, dispatch); + +function DonatePage({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + executeGA = () => {}, + isDonating = false, + showLoading, + t +}: DonatePageProps) { + useEffect(() => { + executeGA({ + type: 'event', + data: { + category: 'Donation View', + action: `Displayed donate page`, + nonInteraction: true + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function handleProcessing(duration: string, amount: number, action: string) { + executeGA({ + type: 'event', + data: { + category: 'Donation', + action: `donate page ${action}`, + label: duration, + value: amount + } + }); + } + + return showLoading ? ( + + ) : ( + <> + + + {/* 'Spacer' cannot be used as a JSX component. */} + {/* Its return type 'Element | Element[]' is not a valid JSX element. */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + + <> + + + + {isDonating ? ( +

{t('donate.thank-you')}

+ ) : ( +

{t('donate.help-more')}

+ )} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + +
+ {isDonating ? ( + +

{t('donate.thank-you-2')}

+
+ +
+ ) : null} + + + + +
+ + + +
+ + + + + +
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + +
+ + ); +} + +DonatePage.displayName = 'DonatePage'; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withTranslation()(DonatePage)); diff --git a/client/src/pages/email-sign-up.js b/client/src/pages/email-sign-up.js deleted file mode 100644 index 0fb0041ab6..0000000000 --- a/client/src/pages/email-sign-up.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import SectionHeader from '../components/settings/SectionHeader'; -import IntroDescription from '../components/Intro/components/IntroDescription'; -import { withTranslation } from 'react-i18next'; - -import { Row, Col, Button, Grid } from '@freecodecamp/react-bootstrap'; -import Helmet from 'react-helmet'; -import { createSelector } from 'reselect'; - -import { ButtonSpacer, Spacer } from '../components/helpers'; -import { acceptTerms, userSelector } from '../redux'; -import createRedirect from '../components/createRedirect'; - -import './email-sign-up.css'; - -const propTypes = { - acceptTerms: PropTypes.func.isRequired, - acceptedPrivacyTerms: PropTypes.bool, - isSignedIn: PropTypes.bool, - t: PropTypes.func.isRequired -}; - -const mapStateToProps = createSelector( - userSelector, - ({ acceptedPrivacyTerms }) => ({ - acceptedPrivacyTerms - }) -); -const mapDispatchToProps = dispatch => - bindActionCreators({ acceptTerms }, dispatch); -const RedirectToLearn = createRedirect('/learn'); - -class AcceptPrivacyTerms extends Component { - componentWillUnmount() { - // if a user navigates away from here we should set acceptedPrivacyTerms - // to true (so they do not get pulled back) without changing their email - // preferences (hence the null payload) - // This ensures the user has to click the checkbox and then click the - // 'Continue...' button to sign up. - if (!this.props.acceptedPrivacyTerms) { - this.props.acceptTerms(null); - } - } - - onClick(isWeeklyEmailAccepted) { - this.props.acceptTerms(isWeeklyEmailAccepted); - } - - render() { - const { acceptedPrivacyTerms, t } = this.props; - if (acceptedPrivacyTerms) { - return ; - } - - return ( - - - {t('misc.email-signup')} | freeCodeCamp.org - - - - {t('misc.email-signup')} - - - - {t('misc.quincy')} - -

{t('misc.email-blast')}

- - - - - - - - - - - - - - -
-
-
- ); - } -} - -AcceptPrivacyTerms.propTypes = propTypes; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withTranslation()(AcceptPrivacyTerms)); diff --git a/client/src/pages/email-sign-up.tsx b/client/src/pages/email-sign-up.tsx new file mode 100644 index 0000000000..3cb2a76166 --- /dev/null +++ b/client/src/pages/email-sign-up.tsx @@ -0,0 +1,117 @@ +import React, { useEffect } from 'react'; +import { bindActionCreators } from 'redux'; +import type { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import SectionHeader from '../components/settings/SectionHeader'; +import IntroDescription from '../components/Intro/components/IntroDescription'; +import { withTranslation } from 'react-i18next'; + +import { Row, Col, Button, Grid } from '@freecodecamp/react-bootstrap'; +import Helmet from 'react-helmet'; +import { createSelector } from 'reselect'; + +import { ButtonSpacer, Spacer } from '../components/helpers'; +import { acceptTerms, userSelector } from '../redux'; +import createRedirect from '../components/createRedirect'; + +import './email-sign-up.css'; + +interface AcceptPrivacyTermsProps { + acceptTerms: (accept: boolean | null) => void; + acceptedPrivacyTerms: boolean; + t: (s: string) => string; +} + +const mapStateToProps = createSelector( + userSelector, + ({ acceptedPrivacyTerms }: { acceptedPrivacyTerms: boolean }) => ({ + acceptedPrivacyTerms + }) +); +const mapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators({ acceptTerms }, dispatch); +const RedirectToLearn = createRedirect('/learn'); + +function AcceptPrivacyTerms({ + acceptTerms, + acceptedPrivacyTerms, + t +}: AcceptPrivacyTermsProps) { + // if a user navigates away from here we should set acceptedPrivacyTerms + // to true (so they do not get pulled back) without changing their email + // preferences (hence the null payload) + // This ensures the user has to click the checkbox and then click the + // 'Continue...' button to sign up. + useEffect(() => { + return () => { + if (!acceptedPrivacyTerms) { + acceptTerms(null); + } + }; + }, [acceptTerms, acceptedPrivacyTerms]); + + function onClick(isWeeklyEmailAccepted: boolean) { + acceptTerms(isWeeklyEmailAccepted); + } + + return acceptedPrivacyTerms ? ( + + ) : ( + <> + + {t('misc.email-signup')} | freeCodeCamp.org + + + {t('misc.email-signup')} + + + + {t('misc.quincy')} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + +

{t('misc.email-blast')}

+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + + + + + + + + + + + + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + + +
+
+ + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withTranslation()(AcceptPrivacyTerms)); diff --git a/client/src/pages/index.js b/client/src/pages/index.js deleted file mode 100644 index 40b683a191..0000000000 --- a/client/src/pages/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Landing from '../components/landing'; -import { AllChallengeNode } from '../redux/prop-types'; - -const IndexPage = () => { - return ; -}; - -const propTypes = { - data: PropTypes.shape({ - allChallengeNode: AllChallengeNode - }) -}; - -IndexPage.propTypes = propTypes; -IndexPage.displayName = 'IndexPage'; - -export default IndexPage; diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx new file mode 100644 index 0000000000..38b541bd5a --- /dev/null +++ b/client/src/pages/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import Landing from '../components/landing'; + +function IndexPage(): JSX.Element { + return ; +} + +IndexPage.displayName = 'IndexPage'; + +export default IndexPage; diff --git a/client/src/pages/learn.js b/client/src/pages/learn.tsx similarity index 73% rename from client/src/pages/learn.js rename to client/src/pages/learn.tsx index 8fe74307a8..f6f5ed7e1b 100644 --- a/client/src/pages/learn.js +++ b/client/src/pages/learn.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Grid, Row, Col } from '@freecodecamp/react-bootstrap'; -import PropTypes from 'prop-types'; import { createSelector } from 'reselect'; import { graphql } from 'gatsby'; import Helmet from 'react-helmet'; @@ -16,38 +15,47 @@ import { isSignedInSelector, userSelector } from '../redux'; -import { ChallengeNode } from '../redux/prop-types'; + +interface FetchState { + pending: boolean; + complete: boolean; + errored: boolean; +} + +interface User { + name: string; + username: string; + completedChallengeCount: number; +} const mapStateToProps = createSelector( userFetchStateSelector, isSignedInSelector, userSelector, - (fetchState, isSignedIn, user) => ({ + (fetchState: FetchState, isSignedIn: boolean, user: User) => ({ fetchState, isSignedIn, user }) ); -const propTypes = { - data: PropTypes.shape({ - challengeNode: ChallengeNode - }), - fetchState: PropTypes.shape({ - pending: PropTypes.bool, - complete: PropTypes.bool, - errored: PropTypes.bool - }), - isSignedIn: PropTypes.bool, - state: PropTypes.object, - user: PropTypes.shape({ - name: PropTypes.string, - username: PropTypes.string, - completedChallengeCount: PropTypes.number - }) -}; +interface Slug { + slug: string; +} -const LearnPage = ({ +interface LearnPageProps { + isSignedIn: boolean; + fetchState: FetchState; + state: Record; + user: User; + data: { + challengeNode: { + fields: Slug; + }; + }; +} + +function LearnPage({ isSignedIn, fetchState: { pending, complete }, user: { name = '', completedChallengeCount = 0 }, @@ -56,7 +64,7 @@ const LearnPage = ({ fields: { slug } } } -}) => { +}: LearnPageProps) { const { t } = useTranslation(); return ( @@ -74,16 +82,17 @@ const LearnPage = ({ slug={slug} /> + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} ); -}; +} LearnPage.displayName = 'LearnPage'; -LearnPage.propTypes = propTypes; export default connect(mapStateToProps)(LearnPage); diff --git a/client/src/pages/settings.js b/client/src/pages/settings.tsx similarity index 76% rename from client/src/pages/settings.js rename to client/src/pages/settings.tsx index c89a0d47e0..2a5957a583 100644 --- a/client/src/pages/settings.js +++ b/client/src/pages/settings.tsx @@ -5,10 +5,12 @@ import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; import ShowSettings from '../client-only-routes/show-settings'; -function Settings() { +function Settings(): JSX.Element { return ( + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} ); diff --git a/client/src/pages/unsubscribed.js b/client/src/pages/unsubscribed.tsx similarity index 61% rename from client/src/pages/unsubscribed.js rename to client/src/pages/unsubscribed.tsx index 2670a37174..7131356e39 100644 --- a/client/src/pages/unsubscribed.js +++ b/client/src/pages/unsubscribed.tsx @@ -5,11 +5,17 @@ import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; import ShowUnsubscribed from '../client-only-routes/show-unsubscribed'; -function Unsubscribed() { +function Unsubscribed(): JSX.Element { return ( + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} ); diff --git a/client/src/pages/update-email.js b/client/src/pages/update-email.js deleted file mode 100644 index 6c0b63814e..0000000000 --- a/client/src/pages/update-email.js +++ /dev/null @@ -1,144 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'gatsby'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { - Form, - FormGroup, - FormControl, - ControlLabel, - Grid, - Row, - Col, - Button -} from '@freecodecamp/react-bootstrap'; -import Helmet from 'react-helmet'; -import isEmail from 'validator/lib/isEmail'; -import { isString } from 'lodash-es'; -import { withTranslation } from 'react-i18next'; - -import { Spacer } from '../components/helpers'; -import './update-email.css'; -import { userSelector } from '../redux'; -import { updateMyEmail } from '../redux/settings'; -import { maybeEmailRE } from '../utils'; - -const propTypes = { - isNewEmail: PropTypes.bool, - t: PropTypes.func.isRequired, - updateMyEmail: PropTypes.func.isRequired -}; - -const mapStateToProps = createSelector( - userSelector, - ({ email, emailVerified }) => ({ - isNewEmail: !email || emailVerified - }) -); - -const mapDispatchToProps = dispatch => - bindActionCreators({ updateMyEmail }, dispatch); - -class UpdateEmail extends Component { - constructor(props) { - super(props); - - this.state = { - emailValue: '' - }; - - this.onChange = this.onChange.bind(this); - } - - handleSubmit = e => { - e.preventDefault(); - const { emailValue } = this.state; - const { updateMyEmail } = this.props; - return updateMyEmail(emailValue); - }; - - onChange(e) { - const change = e.target.value; - if (!isString(change)) { - return null; - } - return this.setState({ - emailValue: change - }); - } - - getEmailValidationState() { - const { emailValue } = this.state; - if (maybeEmailRE.test(emailValue)) { - return isEmail(emailValue) ? 'success' : 'error'; - } - return null; - } - - render() { - const { isNewEmail, t } = this.props; - return ( - - - {t('misc.update-email-1')} | freeCodeCamp.org - - -

{t('misc.update-email-2')}

- - - - -
- - - {t('misc.email')} - - - - - - -
-

- {t('buttons.sign-out')} -

-
- -
-
-
- ); - } -} - -UpdateEmail.displayName = 'Update-Email'; -UpdateEmail.propTypes = propTypes; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withTranslation()(UpdateEmail)); diff --git a/client/src/pages/update-email.tsx b/client/src/pages/update-email.tsx new file mode 100644 index 0000000000..2fa6226224 --- /dev/null +++ b/client/src/pages/update-email.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import type { FormEvent, ChangeEvent } from 'react'; +import { Link } from 'gatsby'; +import { bindActionCreators } from 'redux'; +import type { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { + Form, + FormGroup, + FormControl, + ControlLabel, + Grid, + Row, + Col, + Button +} from '@freecodecamp/react-bootstrap'; +import Helmet from 'react-helmet'; +import isEmail from 'validator/lib/isEmail'; +import { isString } from 'lodash-es'; +import { withTranslation } from 'react-i18next'; + +import { Spacer } from '../components/helpers'; +import './update-email.css'; +import { userSelector } from '../redux'; +import { updateMyEmail } from '../redux/settings'; +import { maybeEmailRE } from '../utils'; + +interface UpdateEmailProps { + isNewEmail: boolean; + t: (s: string) => string; + updateMyEmail: (e: string) => void; +} + +const mapStateToProps = createSelector( + userSelector, + ({ email, emailVerified }: { email: string; emailVerified: boolean }) => ({ + isNewEmail: !email || emailVerified + }) +); + +const mapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators({ updateMyEmail }, dispatch); + +function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) { + const [emailValue, setEmailValue] = useState(''); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + updateMyEmail(emailValue); + } + + function onChange(event: ChangeEvent) { + const change = (event.target as HTMLInputElement).value; + if (!isString(change)) { + return null; + } + setEmailValue(change); + return null; + } + + function getEmailValidationState() { + if (maybeEmailRE.test(emailValue)) { + return isEmail(emailValue) ? 'success' : 'error'; + } + return null; + } + + return ( + <> + + {t('misc.update-email-1')} | freeCodeCamp.org + + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + +

{t('misc.update-email-2')}

+ + + + +
+ + + {t('misc.email')} + + + + + + +
+

+ {t('buttons.sign-out')} +

+
+ +
+
+ + ); +} + +UpdateEmail.displayName = 'Update-Email'; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withTranslation()(UpdateEmail)); diff --git a/client/src/pages/user.js b/client/src/pages/user.tsx similarity index 76% rename from client/src/pages/user.js rename to client/src/pages/user.tsx index 1717ae3c27..9ac7eb1140 100644 --- a/client/src/pages/user.js +++ b/client/src/pages/user.tsx @@ -5,10 +5,12 @@ import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; import ShowUser from '../client-only-routes/show-user'; -function User() { +function User(): JSX.Element { return ( + {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} );