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';
|
||||
/* 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>
|
||||
);
|
@ -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 { 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);
|
||||
|
@ -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>
|
||||
);
|
@ -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>
|
||||
);
|
@ -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 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>
|
||||
);
|
Reference in New Issue
Block a user