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 <shauhami020@gmail.com>
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
awu43
2021-06-25 08:32:57 -07:00
committed by Mrugesh Mohapatra
parent 8c1084a97b
commit e54e2588bf
18 changed files with 524 additions and 495 deletions

View File

@ -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 (
<Router>
{/* Error from installing @types/react-helmet and @types/react-redux */}
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<ShowProfileOrFourOhFour path={withPrefix('/:maybeUser')} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<FourOhFour default={true} />
</Router>
);

View File

@ -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 (
<Router>
<ShowCertification
path={withPrefix('/certification/:username/:certSlug')}
/>
<RedirectHome default={true} />
</Router>
);
}
}
export default Certification;

View File

@ -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 (
<Router>
<ShowCertification
// Error from installing @types/react-helmet and @types/react-redux
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={withPrefix('/certification/:username/:certSlug')}
/>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<RedirectHome default={true} />
</Router>
);
}
export default Certification;

View File

@ -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 = () => (
<Router basepath={withPrefix('/challenges')}>
<Redirect path='/:superBlock/' />
<Redirect path='/:superBlock/:block/' />
<Redirect path='/:superBlock/:block/:challenge' />
<Redirect default={true} />
</Router>
);
Challenges.displayName = 'Challenges';
export default Challenges;

View File

@ -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 (
<Router basepath={withPrefix('/challenges')}>
<Redirect path='/:superBlock/' />
<Redirect path='/:superBlock/:block/' />
<Redirect path='/:superBlock/:block/:challenge' />
<Redirect default={true} />
</Router>
);
}
Challenges.displayName = 'Challenges';
export default Challenges;

View File

@ -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 <Loader fullScreen={true} />;
}
return (
<Fragment>
<Helmet title={`${t('donate.title')} | freeCodeCamp.org`} />
<Grid className='donate-page-wrapper'>
<Spacer />
<Row>
<Fragment>
<Col lg={6} lgOffset={0} md={8} mdOffset={2} sm={10} smOffset={1}>
<Row>
<Col className={'text-center'} xs={12}>
{isDonating ? (
<h2>{t('donate.thank-you')}</h2>
) : (
<h2>{t('donate.help-more')}</h2>
)}
<Spacer />
</Col>
</Row>
{isDonating ? (
<Alert>
<p>{t('donate.thank-you-2')}</p>
<br />
<DonationOptionsAlertText />
</Alert>
) : null}
<DonationText />
<DonateForm
enableDonationSettingsPage={this.enableDonationSettingsPage}
handleProcessing={this.handleProcessing}
/>
<Row className='donate-support'>
<Col xs={12}>
<hr />
<DonationOptionsText />
<DonationSupportText />
</Col>
</Row>
</Col>
<Col lg={6}>
<CampersImage page='donate' />
</Col>
</Fragment>
</Row>
<Spacer />
</Grid>
</Fragment>
);
}
}
DonatePage.displayName = 'DonatePage';
DonatePage.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(DonatePage));

142
client/src/pages/donate.tsx Normal file
View File

@ -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 ? (
<Loader fullScreen={true} />
) : (
<>
<Helmet title={`${t('donate.title')} | freeCodeCamp.org`} />
<Grid className='donate-page-wrapper'>
{/* '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 */}
<Spacer />
<Row>
<>
<Col lg={6} lgOffset={0} md={8} mdOffset={2} sm={10} smOffset={1}>
<Row>
<Col className={'text-center'} xs={12}>
{isDonating ? (
<h2>{t('donate.thank-you')}</h2>
) : (
<h2>{t('donate.help-more')}</h2>
)}
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Col>
</Row>
{isDonating ? (
<Alert>
<p>{t('donate.thank-you-2')}</p>
<br />
<DonationOptionsAlertText />
</Alert>
) : null}
<DonationText />
<DonateForm handleProcessing={handleProcessing} />
<Row className='donate-support'>
<Col xs={12}>
<hr />
<DonationOptionsText />
<DonationSupportText />
</Col>
</Row>
</Col>
<Col lg={6}>
<CampersImage page='donate' />
</Col>
</>
</Row>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Grid>
</>
);
}
DonatePage.displayName = 'DonatePage';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(DonatePage));

View File

@ -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 <RedirectToLearn />;
}
return (
<Fragment>
<Helmet>
<title>{t('misc.email-signup')} | freeCodeCamp.org</title>
</Helmet>
<Grid className='default-page-wrapper email-sign-up'>
<Spacer />
<SectionHeader>{t('misc.email-signup')}</SectionHeader>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<IntroDescription />
<strong>{t('misc.quincy')}</strong>
<Spacer />
<p>{t('misc.email-blast')}</p>
<Spacer />
</Col>
<Col md={4} mdOffset={2} sm={5} smOffset={1} xs={12}>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='big-cta-btn'
onClick={() => this.onClick(true)}
>
{t('buttons.yes-please')}
</Button>
<ButtonSpacer />
</Col>
<Col md={4} sm={5} xs={12}>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='big-cta-btn'
onClick={() => this.onClick(false)}
>
{t('buttons.no-thanks')}
</Button>
<ButtonSpacer />
</Col>
<Col xs={12}>
<Spacer />
</Col>
</Row>
</Grid>
</Fragment>
);
}
}
AcceptPrivacyTerms.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(AcceptPrivacyTerms));

View File

@ -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 ? (
<RedirectToLearn />
) : (
<>
<Helmet>
<title>{t('misc.email-signup')} | freeCodeCamp.org</title>
</Helmet>
<Grid className='default-page-wrapper email-sign-up'>
<SectionHeader>{t('misc.email-signup')}</SectionHeader>
<Row>
<IntroDescription />
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<strong>{t('misc.quincy')}</strong>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<p>{t('misc.email-blast')}</p>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Col>
<Col md={4} mdOffset={2} sm={5} smOffset={1} xs={12}>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='big-cta-btn'
onClick={() => onClick(true)}
>
{t('buttons.yes-please')}
</Button>
<ButtonSpacer />
</Col>
<Col md={4} sm={5} xs={12}>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='big-cta-btn'
onClick={() => onClick(false)}
>
{t('buttons.no-thanks')}
</Button>
<ButtonSpacer />
</Col>
<Col xs={12}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Col>
</Row>
</Grid>
</>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(AcceptPrivacyTerms));

View File

@ -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 <Landing />;
};
const propTypes = {
data: PropTypes.shape({
allChallengeNode: AllChallengeNode
})
};
IndexPage.propTypes = propTypes;
IndexPage.displayName = 'IndexPage';
export default IndexPage;

View File

@ -0,0 +1,11 @@
import React from 'react';
import Landing from '../components/landing';
function IndexPage(): JSX.Element {
return <Landing />;
}
IndexPage.displayName = 'IndexPage';
export default IndexPage;

View File

@ -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<string, unknown>;
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}
/>
<Map />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer size={2} />
</Col>
</Row>
</Grid>
</LearnLayout>
);
};
}
LearnPage.displayName = 'LearnPage';
LearnPage.propTypes = propTypes;
export default connect(mapStateToProps)(LearnPage);

View File

@ -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 (
<Router>
<ShowSettings path={withPrefix('/settings')} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<RedirectHome default={true} />
</Router>
);

View File

@ -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 (
<Router>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<ShowUnsubscribed path={withPrefix('/unsubscribed/:unsubscribeId')} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<ShowUnsubscribed path={withPrefix('/unsubscribed')} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<RedirectHome default={true} />
</Router>
);

View File

@ -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 (
<Fragment>
<Helmet>
<title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
</Helmet>
<Spacer />
<h2 className='text-center'>{t('misc.update-email-2')}</h2>
<Grid>
<Row>
<Col sm={6} smOffset={3}>
<Row>
<Form horizontal={true} onSubmit={this.handleSubmit}>
<FormGroup
controlId='emailInput'
validationState={this.getEmailValidationState()}
>
<Col
className='email-label'
componentClass={ControlLabel}
sm={2}
>
{t('misc.email')}
</Col>
<Col sm={10}>
<FormControl
onChange={this.onChange}
placeholder='camperbot@example.com'
required={true}
type='email'
/>
</Col>
</FormGroup>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
disabled={this.getEmailValidationState() !== 'success'}
type='submit'
>
{isNewEmail
? t('buttons.update-email')
: t('buttons.verify-email')}
</Button>
</Form>
<p className='text-center'>
<Link to='/signout'>{t('buttons.sign-out')}</Link>
</p>
</Row>
</Col>
</Row>
</Grid>
</Fragment>
);
}
}
UpdateEmail.displayName = 'Update-Email';
UpdateEmail.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(UpdateEmail));

View File

@ -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 (
<>
<Helmet>
<title>{t('misc.update-email-1')} | freeCodeCamp.org</title>
</Helmet>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<h2 className='text-center'>{t('misc.update-email-2')}</h2>
<Grid>
<Row>
<Col sm={6} smOffset={3}>
<Row>
<Form horizontal={true} onSubmit={handleSubmit}>
<FormGroup
controlId='emailInput'
validationState={getEmailValidationState()}
>
<Col
className='email-label'
// TODO
componentClass={ControlLabel as unknown}
sm={2}
>
{t('misc.email')}
</Col>
<Col sm={10}>
<FormControl
onChange={onChange}
placeholder='camperbot@example.com'
required={true}
type='email'
/>
</Col>
</FormGroup>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
disabled={getEmailValidationState() !== 'success'}
type='submit'
>
{isNewEmail
? t('buttons.update-email')
: t('buttons.verify-email')}
</Button>
</Form>
<p className='text-center'>
<Link to='/signout'>{t('buttons.sign-out')}</Link>
</p>
</Row>
</Col>
</Row>
</Grid>
</>
);
}
UpdateEmail.displayName = 'Update-Email';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(UpdateEmail));

View File

@ -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 (
<Router>
<ShowUser path={withPrefix('/user/:username/report-user')} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<RedirectHome default={true} />
</Router>
);