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:
@ -7,10 +7,15 @@ import FourOhFour from '../components/FourOhFour';
|
|||||||
import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four';
|
import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four';
|
||||||
/* eslint-enable max-len */
|
/* eslint-enable max-len */
|
||||||
|
|
||||||
function FourOhFourPage() {
|
function FourOhFourPage(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<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')} />
|
<ShowProfileOrFourOhFour path={withPrefix('/:maybeUser')} />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<FourOhFour default={true} />
|
<FourOhFour default={true} />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
@ -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;
|
|
26
client/src/pages/certification.tsx
Normal file
26
client/src/pages/certification.tsx
Normal 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;
|
@ -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;
|
|
44
client/src/pages/challenges.tsx
Normal file
44
client/src/pages/challenges.tsx
Normal 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;
|
@ -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
142
client/src/pages/donate.tsx
Normal 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));
|
@ -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));
|
|
117
client/src/pages/email-sign-up.tsx
Normal file
117
client/src/pages/email-sign-up.tsx
Normal 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));
|
@ -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;
|
|
11
client/src/pages/index.tsx
Normal file
11
client/src/pages/index.tsx
Normal 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;
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
@ -16,38 +15,47 @@ import {
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userSelector
|
userSelector
|
||||||
} from '../redux';
|
} 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(
|
const mapStateToProps = createSelector(
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(fetchState, isSignedIn, user) => ({
|
(fetchState: FetchState, isSignedIn: boolean, user: User) => ({
|
||||||
fetchState,
|
fetchState,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
user
|
user
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const propTypes = {
|
interface Slug {
|
||||||
data: PropTypes.shape({
|
slug: string;
|
||||||
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
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const LearnPage = ({
|
interface LearnPageProps {
|
||||||
|
isSignedIn: boolean;
|
||||||
|
fetchState: FetchState;
|
||||||
|
state: Record<string, unknown>;
|
||||||
|
user: User;
|
||||||
|
data: {
|
||||||
|
challengeNode: {
|
||||||
|
fields: Slug;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function LearnPage({
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
fetchState: { pending, complete },
|
fetchState: { pending, complete },
|
||||||
user: { name = '', completedChallengeCount = 0 },
|
user: { name = '', completedChallengeCount = 0 },
|
||||||
@ -56,7 +64,7 @@ const LearnPage = ({
|
|||||||
fields: { slug }
|
fields: { slug }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) => {
|
}: LearnPageProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,16 +82,17 @@ const LearnPage = ({
|
|||||||
slug={slug}
|
slug={slug}
|
||||||
/>
|
/>
|
||||||
<Map />
|
<Map />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<Spacer size={2} />
|
<Spacer size={2} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LearnLayout>
|
</LearnLayout>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
LearnPage.displayName = 'LearnPage';
|
LearnPage.displayName = 'LearnPage';
|
||||||
LearnPage.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(LearnPage);
|
export default connect(mapStateToProps)(LearnPage);
|
||||||
|
|
@ -5,10 +5,12 @@ import { withPrefix } from 'gatsby';
|
|||||||
import RedirectHome from '../components/RedirectHome';
|
import RedirectHome from '../components/RedirectHome';
|
||||||
import ShowSettings from '../client-only-routes/show-settings';
|
import ShowSettings from '../client-only-routes/show-settings';
|
||||||
|
|
||||||
function Settings() {
|
function Settings(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<ShowSettings path={withPrefix('/settings')} />
|
<ShowSettings path={withPrefix('/settings')} />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<RedirectHome default={true} />
|
<RedirectHome default={true} />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
@ -5,11 +5,17 @@ import { withPrefix } from 'gatsby';
|
|||||||
import RedirectHome from '../components/RedirectHome';
|
import RedirectHome from '../components/RedirectHome';
|
||||||
import ShowUnsubscribed from '../client-only-routes/show-unsubscribed';
|
import ShowUnsubscribed from '../client-only-routes/show-unsubscribed';
|
||||||
|
|
||||||
function Unsubscribed() {
|
function Unsubscribed(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<ShowUnsubscribed path={withPrefix('/unsubscribed/:unsubscribeId')} />
|
<ShowUnsubscribed path={withPrefix('/unsubscribed/:unsubscribeId')} />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<ShowUnsubscribed path={withPrefix('/unsubscribed')} />
|
<ShowUnsubscribed path={withPrefix('/unsubscribed')} />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<RedirectHome default={true} />
|
<RedirectHome default={true} />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
@ -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));
|
|
132
client/src/pages/update-email.tsx
Normal file
132
client/src/pages/update-email.tsx
Normal 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));
|
@ -5,10 +5,12 @@ import { withPrefix } from 'gatsby';
|
|||||||
import RedirectHome from '../components/RedirectHome';
|
import RedirectHome from '../components/RedirectHome';
|
||||||
import ShowUser from '../client-only-routes/show-user';
|
import ShowUser from '../client-only-routes/show-user';
|
||||||
|
|
||||||
function User() {
|
function User(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<ShowUser path={withPrefix('/user/:username/report-user')} />
|
<ShowUser path={withPrefix('/user/:username/report-user')} />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
<RedirectHome default={true} />
|
<RedirectHome default={true} />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
Reference in New Issue
Block a user