feat(client): convert class components to functional components (#43226)

This commit is contained in:
awu43
2021-10-26 22:47:47 -07:00
committed by GitHub
parent fbc0ea8742
commit fa9fb61f6a
10 changed files with 632 additions and 747 deletions

View File

@ -7,7 +7,7 @@ import {
Col, Col,
Row Row
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react'; import React, { useState } from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { TFunction, Trans, withTranslation } from 'react-i18next'; import { TFunction, Trans, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -58,111 +58,94 @@ const mapDispatchToProps = {
reportUser reportUser
}; };
class ShowUser extends Component<IShowUserProps> { function ShowUser({
state: { email,
textarea: string; isSignedIn,
}; reportUser,
constructor(props: IShowUserProps) { t,
super(props); userFetchState,
username
}: IShowUserProps) {
const [textarea, setTextarea] = useState('');
this.state = { function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
textarea: '' setTextarea(e.target.value.slice());
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
} }
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) { function handleSubmit(e: React.FormEvent) {
const textarea = e.target.value.slice();
return this.setState({
textarea
});
}
handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
const { textarea: reportDescription } = this.state; reportUser({ username, reportDescription: textarea });
const { username, reportUser } = this.props;
return reportUser({ username, reportDescription });
} }
render() { const { pending, complete, errored } = userFetchState;
const { username, isSignedIn, userFetchState, email, t } = this.props; if (pending && !complete) {
const { pending, complete, errored } = userFetchState; return <Loader fullScreen={true} />;
if (pending && !complete) { }
return <Loader fullScreen={true} />;
}
if ((complete || errored) && !isSignedIn) { if ((complete || errored) && !isSignedIn) {
return (
<main>
<FullWidthRow>
<Spacer size={2} />
<Panel bsStyle='info' className='text-center'>
<Panel.Heading>
<Panel.Title componentClass='h3'>
{t('report.sign-in')}
</Panel.Title>
</Panel.Heading>
<Panel.Body className='text-center'>
<Spacer size={2} />
<Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
<Login block={true}>{t('buttons.click-here')}</Login>
</Col>
<Spacer size={3} />
</Panel.Body>
</Panel>
</FullWidthRow>
</main>
);
}
const { textarea } = this.state;
const placeholderText = t('report.details');
return ( return (
<> <main>
<Helmet> <FullWidthRow>
<title>{t('report.portfolio')} | freeCodeCamp.org</title> <Spacer size={2} />
</Helmet> <Panel bsStyle='info' className='text-center'>
<Spacer size={2} /> <Panel.Heading>
<Row className='text-center overflow-fix'> <Panel.Title componentClass='h3'>
<Col sm={8} smOffset={2} xs={12}> {t('report.sign-in')}
<h2>{t('report.portfolio-2', { username: username })}</h2> </Panel.Title>
</Col> </Panel.Heading>
</Row> <Panel.Body className='text-center'>
<Row className='overflow-fix'> <Spacer size={2} />
<Col sm={6} smOffset={3} xs={12}> <Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
<p> <Login block={true}>{t('buttons.click-here')}</Login>
<Trans i18nKey='report.notify-1'> </Col>
<strong>{{ email }}</strong> <Spacer size={3} />
</Trans> </Panel.Body>
</p> </Panel>
<p>{t('report.notify-2')}</p> </FullWidthRow>
{/* eslint-disable @typescript-eslint/unbound-method */} </main>
<form onSubmit={this.handleSubmit}>
<FormGroup controlId='report-user-textarea'>
<ControlLabel>{t('report.what')}</ControlLabel>
<FormControl
componentClass='textarea'
onChange={this.handleChange}
placeholder={placeholderText}
value={textarea}
/>
</FormGroup>
<Button block={true} bsStyle='primary' type='submit'>
{t('report.submit')}
</Button>
<Spacer />
</form>
{/* eslint-disable @typescript-eslint/unbound-method */}
</Col>
</Row>
</>
); );
} }
return (
<>
<Helmet>
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
</Helmet>
<Spacer size={2} />
<Row className='text-center overflow-fix'>
<Col sm={8} smOffset={2} xs={12}>
<h2>{t('report.portfolio-2', { username: username })}</h2>
</Col>
</Row>
<Row className='overflow-fix'>
<Col sm={6} smOffset={3} xs={12}>
<p>
<Trans i18nKey='report.notify-1'>
<strong>{{ email }}</strong>
</Trans>
</p>
<p>{t('report.notify-2')}</p>
<form onSubmit={handleSubmit}>
<FormGroup controlId='report-user-textarea'>
<ControlLabel>{t('report.what')}</ControlLabel>
<FormControl
componentClass='textarea'
onChange={handleChange}
placeholder={t('report.details')}
value={textarea}
/>
</FormGroup>
<Button block={true} bsStyle='primary' type='submit'>
{t('report.submit')}
</Button>
<Spacer />
</form>
</Col>
</Row>
</>
);
} }
// @ts-expect-error Config might need to be remedied, or component transformed into F.C.
ShowUser.displayName = 'ShowUser'; ShowUser.displayName = 'ShowUser';
export default withTranslation()( export default withTranslation()(

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
@ -52,48 +52,46 @@ type LearnLayoutProps = {
children?: React.ReactNode; children?: React.ReactNode;
}; };
class LearnLayout extends Component<LearnLayoutProps> { function LearnLayout({
static displayName = 'LearnLayout'; isSignedIn,
fetchState,
user,
tryToShowDonationModal,
children
}: LearnLayoutProps): JSX.Element {
useEffect(() => {
tryToShowDonationModal();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
componentDidMount() { useEffect(() => {
this.props.tryToShowDonationModal(); return () => {
const metaTag = document.querySelector(`meta[name="robots"]`);
if (metaTag) {
metaTag.remove();
}
};
}, []);
if (fetchState.pending && !fetchState.complete) {
return <Loader fullScreen={true} />;
} }
componentWillUnmount() { if (isSignedIn && !user.acceptedPrivacyTerms) {
const metaTag = document.querySelector(`meta[name="robots"]`); return <RedirectEmailSignUp />;
if (metaTag) {
metaTag.remove();
}
} }
render() { return (
const { <>
fetchState: { pending, complete }, <Helmet>
isSignedIn, <meta content='noindex' name='robots' />
user: { acceptedPrivacyTerms }, </Helmet>
children <main id='learn-app-wrapper'>{children}</main>
} = this.props; {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore */}
if (pending && !complete) { <DonateModal />
return <Loader fullScreen={true} />; </>
} );
if (isSignedIn && !acceptedPrivacyTerms) {
return <RedirectEmailSignUp />;
}
return (
<>
<Helmet>
<meta content='noindex' name='robots' />
</Helmet>
<main id='learn-app-wrapper'>{children}</main>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore */}
<DonateModal />
</>
);
}
} }
export default connect(mapStateToProps, mapDispatchToProps)(LearnLayout); export default connect(mapStateToProps, mapDispatchToProps)(LearnLayout);

View File

@ -9,7 +9,7 @@ import {
import Loadable from '@loadable/component'; import Loadable from '@loadable/component';
import { useStaticQuery, graphql } from 'gatsby'; import { useStaticQuery, graphql } from 'gatsby';
import { reverse, sortBy } from 'lodash-es'; import { reverse, sortBy } from 'lodash-es';
import React, { Component, useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import { TFunction, withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
@ -66,43 +66,58 @@ interface TimelineInnerProps extends TimelineProps {
totalPages: number; totalPages: number;
} }
interface TimeLineInnerState { function TimelineInner({
solutionToView: string | null; idToNameMap,
solutionOpen: boolean; sortedTimeline,
pageNo: number; totalPages,
solution: string | null;
challengeFiles: ChallengeFiles;
}
class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> { completedMap,
constructor(props: TimelineInnerProps) { t,
super(props); username
}: TimelineInnerProps) {
const [solutionToView, setSolutionToView] = useState<string | null>(null);
const [solutionOpen, setSolutionOpen] = useState(false);
const [pageNo, setPageNo] = useState(1);
const [solution, setSolution] = useState<string | null>(null);
const [challengeFiles, setChallengeFiles] = useState<ChallengeFiles>(null);
this.state = { function viewSolution(
solutionToView: null, id: string,
solutionOpen: false, solution_: string,
pageNo: 1, challengeFiles_: ChallengeFiles
solution: null, ): void {
challengeFiles: null setSolutionToView(id);
}; setSolutionOpen(true);
setSolution(solution_);
this.closeSolution = this.closeSolution.bind(this); setChallengeFiles(challengeFiles_);
this.renderCompletion = this.renderCompletion.bind(this);
this.viewSolution = this.viewSolution.bind(this);
this.firstPage = this.firstPage.bind(this);
this.prevPage = this.prevPage.bind(this);
this.nextPage = this.nextPage.bind(this);
this.lastPage = this.lastPage.bind(this);
this.renderViewButton = this.renderViewButton.bind(this);
} }
renderViewButton( function closeSolution(): void {
setSolutionToView(null);
setSolutionOpen(false);
setSolution(null);
setChallengeFiles(null);
}
function firstPage(): void {
setPageNo(1);
}
function nextPage(): void {
setPageNo(prev => prev + 1);
}
function prevPage(): void {
setPageNo(prev => prev - 1);
}
function lastPage(): void {
setPageNo(totalPages);
}
function renderViewButton(
id: string, id: string,
challengeFiles: ChallengeFiles, challengeFiles: ChallengeFiles,
githubLink: string, githubLink: string,
solution: string solution: string
): React.ReactNode { ): React.ReactNode {
const { t } = this.props;
if (challengeFiles?.length) { if (challengeFiles?.length) {
return ( return (
<Button <Button
@ -110,7 +125,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
id={`btn-for-${id}`} id={`btn-for-${id}`}
onClick={() => this.viewSolution(id, solution, challengeFiles)} onClick={() => viewSolution(id, solution, challengeFiles)}
> >
{t('buttons.show-code')} {t('buttons.show-code')}
</Button> </Button>
@ -163,8 +178,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
} }
} }
renderCompletion(completed: SortedTimeline): JSX.Element { function renderCompletion(completed: SortedTimeline): JSX.Element {
const { idToNameMap, username } = this.props;
const { id, challengeFiles, githubLink, solution } = completed; const { id, challengeFiles, githubLink, solution } = completed;
const completedDate = new Date(completed.completedDate); const completedDate = new Date(completed.completedDate);
// @ts-expect-error idToNameMap is not a <string, string> Map... // @ts-expect-error idToNameMap is not a <string, string> Map...
@ -184,9 +198,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
<Link to={challengePath as string}>{challengeTitle}</Link> <Link to={challengePath as string}>{challengeTitle}</Link>
)} )}
</td> </td>
<td> <td>{renderViewButton(id, challengeFiles, githubLink, solution)}</td>
{this.renderViewButton(id, challengeFiles, githubLink, solution)}
</td>
<td className='text-center'> <td className='text-center'>
<time dateTime={completedDate.toISOString()}> <time dateTime={completedDate.toISOString()}>
{completedDate.toLocaleString([localeCode, 'en-US'], { {completedDate.toLocaleString([localeCode, 'en-US'], {
@ -199,127 +211,72 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
</tr> </tr>
); );
} }
viewSolution(
id: string,
solution: string,
challengeFiles: ChallengeFiles
): void {
this.setState(state => ({
...state,
solutionToView: id,
solutionOpen: true,
solution,
challengeFiles
}));
}
closeSolution() { const id = solutionToView;
this.setState(state => ({ const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
...state, const endIndex = pageNo * ITEMS_PER_PAGE;
solutionToView: null,
solutionOpen: false,
solution: null,
challengeFiles: null
}));
}
firstPage() { return (
this.setState({ <FullWidthRow>
pageNo: 1 <h2 className='text-center'>{t('profile.timeline')}</h2>
}); {completedMap.length === 0 ? (
} <p className='text-center'>
nextPage() { {t('profile.none-completed')}&nbsp;
this.setState(state => ({ <Link to='/learn'>{t('profile.get-started')}</Link>
pageNo: state.pageNo + 1 </p>
})); ) : (
} <Table condensed={true} striped={true}>
<thead>
prevPage() { <tr>
this.setState(state => ({ <th>{t('profile.challenge')}</th>
pageNo: state.pageNo - 1 <th>{t('settings.labels.solution')}</th>
})); <th className='text-center'>{t('profile.completed')}</th>
} </tr>
lastPage() { </thead>
this.setState((_, props) => ({ <tbody>
pageNo: props.totalPages {sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
})); </tbody>
} </Table>
render() { )}
const { {id && (
completedMap, <Modal
idToNameMap, aria-labelledby='contained-modal-title'
username, onHide={closeSolution}
sortedTimeline, show={solutionOpen}
t, >
totalPages = 1 <Modal.Header closeButton={true}>
} = this.props; <Modal.Title id='contained-modal-title'>
const { solutionToView: id, solutionOpen, pageNo = 1 } = this.state; {`${username}'s Solution to ${
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE; // @ts-expect-error Need better TypeDef for this
const endIndex = pageNo * ITEMS_PER_PAGE; idToNameMap.get(id).challengeTitle as string
}`}
return ( </Modal.Title>
<FullWidthRow> </Modal.Header>
<h2 className='text-center'>{t('profile.timeline')}</h2> <Modal.Body>
{completedMap.length === 0 ? ( <SolutionViewer
<p className='text-center'> challengeFiles={challengeFiles}
{t('profile.none-completed')}&nbsp; solution={solution ?? ''}
<Link to='/learn'>{t('profile.get-started')}</Link> />
</p> </Modal.Body>
) : ( <Modal.Footer>
<Table condensed={true} striped={true}> <Button onClick={closeSolution}>{t('buttons.close')}</Button>
<thead> </Modal.Footer>
<tr> </Modal>
<th>{t('profile.challenge')}</th> )}
<th>{t('settings.labels.solution')}</th> {totalPages > 1 && (
<th className='text-center'>{t('profile.completed')}</th> <TimelinePagination
</tr> firstPage={firstPage}
</thead> lastPage={lastPage}
<tbody> nextPage={nextPage}
{sortedTimeline pageNo={pageNo}
.slice(startIndex, endIndex) prevPage={prevPage}
.map(this.renderCompletion)} totalPages={totalPages}
</tbody> />
</Table> )}
)} </FullWidthRow>
{id && ( );
<Modal
aria-labelledby='contained-modal-title'
onHide={this.closeSolution}
show={solutionOpen}
>
<Modal.Header closeButton={true}>
<Modal.Title id='contained-modal-title'>
{`${username}'s Solution to ${
// @ts-expect-error Need better TypeDef for this
idToNameMap.get(id).challengeTitle as string
}`}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SolutionViewer
challengeFiles={this.state.challengeFiles}
solution={this.state.solution ?? ''}
/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeSolution}>{t('buttons.close')}</Button>
</Modal.Footer>
</Modal>
)}
{totalPages > 1 && (
<TimelinePagination
firstPage={this.firstPage}
lastPage={this.lastPage}
nextPage={this.nextPage}
pageNo={pageNo}
prevPage={this.prevPage}
totalPages={totalPages}
/>
)}
</FullWidthRow>
);
}
} }
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
function useIdToNameMap(): Map<string, string> { function useIdToNameMap(): Map<string, string> {
const { const {

View File

@ -1,5 +1,5 @@
import { Button, Panel } from '@freecodecamp/react-bootstrap'; import { Button, Panel } from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react'; import React, { useState } from 'react';
import { TFunction, withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -18,11 +18,6 @@ type DangerZoneProps = {
t: TFunction; t: TFunction;
}; };
type DangerZoneState = {
reset: boolean;
delete: boolean;
};
const mapStateToProps = () => ({}); const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch: Dispatch) => const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators( bindActionCreators(
@ -33,79 +28,65 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
dispatch dispatch
); );
class DangerZone extends Component<DangerZoneProps, DangerZoneState> { function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
static displayName: string; const [reset, setReset] = useState(false);
constructor(props: DangerZoneProps) { const [delete_, setDelete] = useState(false);
super(props); // delete is reserved
this.state = {
reset: false, function toggleResetModal(): void {
delete: false setReset(prev => !prev);
};
} }
toggleResetModal = () => { function toggleDeleteModal(): void {
return this.setState(state => ({ setDelete(prev => !prev);
...state, }
reset: !state.reset
}));
};
toggleDeleteModal = () => { return (
return this.setState(state => ({ <div className='danger-zone text-center'>
...state, <FullWidthRow>
delete: !state.delete <Panel bsStyle='danger'>
})); <Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
}; <Spacer />
<p>{t('settings.danger.be-careful')}</p>
render() { <FullWidthRow>
const { deleteAccount, resetProgress, t } = this.props; <Button
return ( block={true}
<div className='danger-zone text-center'> bsSize='lg'
<FullWidthRow> bsStyle='danger'
<Panel bsStyle='danger'> className='btn-danger'
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading> onClick={toggleResetModal}
type='button'
>
{t('settings.danger.reset')}
</Button>
<ButtonSpacer />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
onClick={toggleDeleteModal}
type='button'
>
{t('settings.danger.delete')}
</Button>
<Spacer /> <Spacer />
<p>{t('settings.danger.be-careful')}</p> </FullWidthRow>
<FullWidthRow> </Panel>
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
onClick={() => this.toggleResetModal()}
type='button'
>
{t('settings.danger.reset')}
</Button>
<ButtonSpacer />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
onClick={() => this.toggleDeleteModal()}
type='button'
>
{t('settings.danger.delete')}
</Button>
<Spacer />
</FullWidthRow>
</Panel>
<ResetModal <ResetModal
onHide={() => this.toggleResetModal()} onHide={toggleResetModal}
reset={resetProgress} reset={resetProgress}
show={this.state.reset} show={reset}
/> />
<DeleteModal <DeleteModal
delete={deleteAccount} delete={deleteAccount}
onHide={() => this.toggleDeleteModal()} onHide={toggleDeleteModal}
show={this.state.delete} show={delete_}
/> />
</FullWidthRow> </FullWidthRow>
</div> </div>
); );
}
} }
DangerZone.displayName = 'DangerZone'; DangerZone.displayName = 'DangerZone';

View File

@ -7,7 +7,7 @@ import {
Button Button
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import { Link } from 'gatsby'; import { Link } from 'gatsby';
import React, { Component } from 'react'; import React, { useState } from 'react';
import { TFunction, Trans, withTranslation } from 'react-i18next'; import { TFunction, Trans, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -36,70 +36,49 @@ type EmailProps = {
updateQuincyEmail: (sendQuincyEmail: boolean) => void; updateQuincyEmail: (sendQuincyEmail: boolean) => void;
}; };
type EmailState = { interface EmailForm {
emailForm: { currentEmail: string;
currentEmail: string; newEmail: string;
newEmail: string; confirmNewEmail: string;
confirmNewEmail: string; isPristine: boolean;
isPristine: boolean;
};
};
export function UpdateEmailButton(this: EmailSettings): JSX.Element {
const { t } = this.props;
return (
<Link style={{ textDecoration: 'none' }} to='/update-email'>
<Button block={true} bsSize='lg' bsStyle='primary'>
{t('buttons.edit')}
</Button>
</Link>
);
} }
class EmailSettings extends Component<EmailProps, EmailState> { function EmailSettings({
static displayName: string; email,
constructor(props: EmailProps) { isEmailVerified,
super(props); sendQuincyEmail,
t,
updateMyEmail,
updateQuincyEmail
}: EmailProps): JSX.Element {
const [emailForm, setEmailForm] = useState<EmailForm>({
currentEmail: email,
newEmail: '',
confirmNewEmail: '',
isPristine: true
});
this.state = { function handleSubmit(e: React.FormEvent): void {
emailForm: { e.preventDefault();
currentEmail: props.email, updateMyEmail(emailForm.newEmail);
newEmail: '', }
confirmNewEmail: '',
isPristine: true function createHandleEmailFormChange(
} key: 'newEmail' | 'confirmNewEmail'
): (e: React.ChangeEvent<HTMLInputElement>) => void {
return e => {
e.preventDefault();
const userInput = e.target.value.slice();
setEmailForm(prev => ({
...prev,
[key]: userInput,
isPristine: userInput === prev.currentEmail
}));
}; };
} }
handleSubmit = (e: React.FormEvent) => { function getValidationForNewEmail() {
e.preventDefault(); const { newEmail, currentEmail } = emailForm;
const {
emailForm: { newEmail }
} = this.state;
const { updateMyEmail } = this.props;
return updateMyEmail(newEmail);
};
createHandleEmailFormChange =
(key: 'newEmail' | 'confirmNewEmail') =>
(e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault();
const userInput = (e.target as HTMLInputElement).value.slice();
return this.setState(state => ({
emailForm: {
...state.emailForm,
[key]: userInput,
isPristine: userInput === state.emailForm.currentEmail
}
}));
};
getValidationForNewEmail = () => {
const {
emailForm: { newEmail, currentEmail }
} = this.state;
const { t } = this.props;
if (!maybeEmailRE.test(newEmail)) { if (!maybeEmailRE.test(newEmail)) {
return { return {
state: null, state: null,
@ -120,14 +99,10 @@ class EmailSettings extends Component<EmailProps, EmailState> {
message: t('validation.invalid-email') message: t('validation.invalid-email')
}; };
} }
}; }
getValidationForConfirmEmail = () => {
const {
emailForm: { confirmNewEmail, newEmail }
} = this.state;
const { t } = this.props;
function getValidationForConfirmEmail() {
const { confirmNewEmail, newEmail } = emailForm;
if (!maybeEmailRE.test(newEmail)) { if (!maybeEmailRE.test(newEmail)) {
return { return {
state: null, state: null,
@ -146,115 +121,109 @@ class EmailSettings extends Component<EmailProps, EmailState> {
message: '' message: ''
}; };
} }
}; }
render() { const { newEmail, confirmNewEmail, currentEmail, isPristine } = emailForm;
const {
emailForm: { newEmail, confirmNewEmail, currentEmail, isPristine }
} = this.state;
const { isEmailVerified, updateQuincyEmail, sendQuincyEmail, t } = const { state: newEmailValidation, message: newEmailValidationMessage } =
this.props; getValidationForNewEmail();
const { state: newEmailValidation, message: newEmailValidationMessage } = const {
this.getValidationForNewEmail(); state: confirmEmailValidation,
message: confirmEmailValidationMessage
} = getValidationForConfirmEmail();
const { if (!currentEmail) {
state: confirmEmailValidation,
message: confirmEmailValidationMessage
} = this.getValidationForConfirmEmail();
if (!currentEmail) {
return (
<div>
<FullWidthRow>
<p className='large-p text-center'>{t('settings.email.missing')}</p>
</FullWidthRow>
<FullWidthRow>
<UpdateEmailButton />
</FullWidthRow>
</div>
);
}
return ( return (
<div className='email-settings'> <div>
<SectionHeader>{t('settings.email.heading')}</SectionHeader>
{isEmailVerified ? null : (
<FullWidthRow>
<HelpBlock>
<Alert
bsStyle='info'
className='text-center'
closeLabel={t('buttons.close')}
>
{t('settings.email.not-verified')}
<br />
<Trans i18nKey='settings.email.check'>
<Link to='/update-email' />
</Trans>
</Alert>
</HelpBlock>
</FullWidthRow>
)}
<FullWidthRow> <FullWidthRow>
<form id='form-update-email' onSubmit={this.handleSubmit}> <p className='large-p text-center'>{t('settings.email.missing')}</p>
<FormGroup controlId='current-email'>
<ControlLabel>{t('settings.email.current')}</ControlLabel>
<FormControl.Static>{currentEmail}</FormControl.Static>
</FormGroup>
<FormGroup
controlId='new-email'
validationState={newEmailValidation}
>
<ControlLabel>{t('settings.email.new')}</ControlLabel>
<FormControl
onChange={this.createHandleEmailFormChange('newEmail')}
type='email'
value={newEmail}
/>
{newEmailValidationMessage ? (
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
) : null}
</FormGroup>
<FormGroup
controlId='confirm-email'
validationState={confirmEmailValidation}
>
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
<FormControl
onChange={this.createHandleEmailFormChange('confirmNewEmail')}
type='email'
value={confirmNewEmail}
/>
{confirmEmailValidationMessage ? (
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
) : null}
</FormGroup>
<BlockSaveButton
disabled={
newEmailValidation !== 'success' ||
confirmEmailValidation !== 'success' ||
isPristine
}
/>
</form>
</FullWidthRow> </FullWidthRow>
<Spacer />
<FullWidthRow> <FullWidthRow>
<form id='form-quincy-email' onSubmit={this.handleSubmit}> <Link style={{ textDecoration: 'none' }} to='/update-email'>
<ToggleSetting <Button block={true} bsSize='lg' bsStyle='primary'>
action={t('settings.email.weekly')} {t('buttons.edit')}
flag={sendQuincyEmail} </Button>
flagName='sendQuincyEmail' </Link>
offLabel={t('buttons.no-thanks')}
onLabel={t('buttons.yes-please')}
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
/>
</form>
</FullWidthRow> </FullWidthRow>
</div> </div>
); );
} }
return (
<div className='email-settings'>
<SectionHeader>{t('settings.email.heading')}</SectionHeader>
{isEmailVerified ? null : (
<FullWidthRow>
<HelpBlock>
<Alert
bsStyle='info'
className='text-center'
closeLabel={t('buttons.close')}
>
{t('settings.email.not-verified')}
<br />
<Trans i18nKey='settings.email.check'>
<Link to='/update-email' />
</Trans>
</Alert>
</HelpBlock>
</FullWidthRow>
)}
<FullWidthRow>
<form id='form-update-email' onSubmit={handleSubmit}>
<FormGroup controlId='current-email'>
<ControlLabel>{t('settings.email.current')}</ControlLabel>
<FormControl.Static>{currentEmail}</FormControl.Static>
</FormGroup>
<FormGroup controlId='new-email' validationState={newEmailValidation}>
<ControlLabel>{t('settings.email.new')}</ControlLabel>
<FormControl
onChange={createHandleEmailFormChange('newEmail')}
type='email'
value={newEmail}
/>
{newEmailValidationMessage ? (
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
) : null}
</FormGroup>
<FormGroup
controlId='confirm-email'
validationState={confirmEmailValidation}
>
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
<FormControl
onChange={createHandleEmailFormChange('confirmNewEmail')}
type='email'
value={confirmNewEmail}
/>
{confirmEmailValidationMessage ? (
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
) : null}
</FormGroup>
<BlockSaveButton
disabled={
newEmailValidation !== 'success' ||
confirmEmailValidation !== 'success' ||
isPristine
}
/>
</form>
</FullWidthRow>
<Spacer />
<FullWidthRow>
<form id='form-quincy-email' onSubmit={handleSubmit}>
<ToggleSetting
action={t('settings.email.weekly')}
flag={sendQuincyEmail}
flagName='sendQuincyEmail'
offLabel={t('buttons.no-thanks')}
onLabel={t('buttons.yes-please')}
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
/>
</form>
</FullWidthRow>
</div>
);
} }
EmailSettings.displayName = 'EmailSettings'; EmailSettings.displayName = 'EmailSettings';

View File

@ -1,5 +1,5 @@
import { Button, Form } from '@freecodecamp/react-bootstrap'; import { Button, Form } from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react'; import React from 'react';
import { TFunction, withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
@ -44,142 +44,145 @@ type PrivacyProps = {
}; };
}; };
class PrivacySettings extends Component<PrivacyProps> { function PrivacySettings({
static displayName: string; submitProfileUI,
t,
handleSubmit = (e: React.FormEvent) => e.preventDefault(); user
}: PrivacyProps): JSX.Element {
toggleFlag = (flag: string) => () => { function handleSubmit(e: React.FormEvent): void {
const privacyValues = { ...this.props.user.profileUI }; e.preventDefault();
privacyValues[flag as keyof ProfileUIType] =
!privacyValues[flag as keyof ProfileUIType];
this.props.submitProfileUI(privacyValues);
};
render() {
const { t, user } = this.props;
const {
isLocked = true,
showAbout = false,
showCerts = false,
showDonation = false,
showHeatMap = false,
showLocation = false,
showName = false,
showPoints = false,
showPortfolio = false,
showTimeLine = false
} = user.profileUI;
return (
<div className='privacy-settings' id='privacy-settings'>
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
<FullWidthRow>
<p>{t('settings.privacy')}</p>
<Form inline={true} onSubmit={this.handleSubmit}>
<ToggleSetting
action={t('settings.labels.my-profile')}
explain={t('settings.disabled')}
flag={isLocked}
flagName='isLocked'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('isLocked')}
/>
<ToggleSetting
action={t('settings.labels.my-name')}
explain={t('settings.private-name')}
flag={!showName}
flagName='name'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showName')}
/>
<ToggleSetting
action={t('settings.labels.my-location')}
flag={!showLocation}
flagName='showLocation'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showLocation')}
/>
<ToggleSetting
action={t('settings.labels.my-about')}
flag={!showAbout}
flagName='showAbout'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showAbout')}
/>
<ToggleSetting
action={t('settings.labels.my-points')}
flag={!showPoints}
flagName='showPoints'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showPoints')}
/>
<ToggleSetting
action={t('settings.labels.my-heatmap')}
flag={!showHeatMap}
flagName='showHeatMap'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showHeatMap')}
/>
<ToggleSetting
action={t('settings.labels.my-certs')}
explain={t('settings.disabled')}
flag={!showCerts}
flagName='showCerts'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showCerts')}
/>
<ToggleSetting
action={t('settings.labels.my-portfolio')}
flag={!showPortfolio}
flagName='showPortfolio'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showPortfolio')}
/>
<ToggleSetting
action={t('settings.labels.my-timeline')}
flag={!showTimeLine}
flagName='showTimeLine'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showTimeLine')}
/>
<ToggleSetting
action={t('settings.labels.my-donations')}
flag={!showDonation}
flagName='showPortfolio'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showDonation')}
/>
</Form>
</FullWidthRow>
<FullWidthRow>
<Spacer />
<p>{t('settings.data')}</p>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
download={`${user.username}.json`}
href={`data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(user)
)}`}
>
{t('buttons.download-data')}
</Button>
</FullWidthRow>
</div>
);
} }
function toggleFlag(flag: string): () => void {
return () => {
const privacyValues = { ...user.profileUI };
privacyValues[flag as keyof ProfileUIType] =
!privacyValues[flag as keyof ProfileUIType];
submitProfileUI(privacyValues);
};
}
const {
isLocked = true,
showAbout = false,
showCerts = false,
showDonation = false,
showHeatMap = false,
showLocation = false,
showName = false,
showPoints = false,
showPortfolio = false,
showTimeLine = false
} = user.profileUI;
return (
<div className='privacy-settings' id='privacy-settings'>
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
<FullWidthRow>
<p>{t('settings.privacy')}</p>
<Form inline={true} onSubmit={handleSubmit}>
<ToggleSetting
action={t('settings.labels.my-profile')}
explain={t('settings.disabled')}
flag={isLocked}
flagName='isLocked'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('isLocked')}
/>
<ToggleSetting
action={t('settings.labels.my-name')}
explain={t('settings.private-name')}
flag={!showName}
flagName='name'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showName')}
/>
<ToggleSetting
action={t('settings.labels.my-location')}
flag={!showLocation}
flagName='showLocation'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showLocation')}
/>
<ToggleSetting
action={t('settings.labels.my-about')}
flag={!showAbout}
flagName='showAbout'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showAbout')}
/>
<ToggleSetting
action={t('settings.labels.my-points')}
flag={!showPoints}
flagName='showPoints'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showPoints')}
/>
<ToggleSetting
action={t('settings.labels.my-heatmap')}
flag={!showHeatMap}
flagName='showHeatMap'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showHeatMap')}
/>
<ToggleSetting
action={t('settings.labels.my-certs')}
explain={t('settings.disabled')}
flag={!showCerts}
flagName='showCerts'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showCerts')}
/>
<ToggleSetting
action={t('settings.labels.my-portfolio')}
flag={!showPortfolio}
flagName='showPortfolio'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showPortfolio')}
/>
<ToggleSetting
action={t('settings.labels.my-timeline')}
flag={!showTimeLine}
flagName='showTimeLine'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showTimeLine')}
/>
<ToggleSetting
action={t('settings.labels.my-donations')}
flag={!showDonation}
flagName='showPortfolio'
offLabel={t('buttons.public')}
onLabel={t('buttons.private')}
toggleFlag={toggleFlag('showDonation')}
/>
</Form>
</FullWidthRow>
<FullWidthRow>
<Spacer />
<p>{t('settings.data')}</p>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
download={`${user.username}.json`}
href={`data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(user)
)}`}
>
{t('buttons.download-data')}
</Button>
</FullWidthRow>
</div>
);
} }
PrivacySettings.displayName = 'PrivacySettings'; PrivacySettings.displayName = 'PrivacySettings';

View File

@ -9,30 +9,35 @@ interface HTMLProps {
preBodyComponents?: React.ReactNode[]; preBodyComponents?: React.ReactNode[];
} }
export default class HTML extends React.Component<HTMLProps> { export default function HTML({
render(): JSX.Element { body,
return ( bodyAttributes,
<html id='__fcc-html' {...this.props.htmlAttributes} lang='en'> headComponents,
<head> htmlAttributes,
<meta charSet='utf-8' /> postBodyComponents,
<meta content='ie=edge' httpEquiv='x-ua-compatible' /> preBodyComponents
<meta }: HTMLProps): JSX.Element {
content='width=device-width, initial-scale=1.0, shrink-to-fit=no' return (
name='viewport' <html id='__fcc-html' {...htmlAttributes} lang='en'>
/> <head>
{this.props.headComponents} <meta charSet='utf-8' />
</head> <meta content='ie=edge' httpEquiv='x-ua-compatible' />
<body {...this.props.bodyAttributes}> <meta
{this.props.preBodyComponents} content='width=device-width, initial-scale=1.0, shrink-to-fit=no'
<div name='viewport'
className='tex2jax_ignore' />
dangerouslySetInnerHTML={{ __html: this.props.body }} {headComponents}
id='___gatsby' </head>
key={'body'} <body {...bodyAttributes}>
/> {preBodyComponents}
{this.props.postBodyComponents} <div
</body> className='tex2jax_ignore'
</html> dangerouslySetInnerHTML={{ __html: body }}
); id='___gatsby'
} key={'body'}
/>
{postBodyComponents}
</body>
</html>
);
} }

View File

@ -1,5 +1,5 @@
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import React, { Component } from 'react'; import React from 'react';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import './output.css'; import './output.css';
@ -9,22 +9,19 @@ interface OutputProps {
output: string[]; output: string[];
} }
class Output extends Component<OutputProps> { function Output({ defaultOutput, output }: OutputProps): JSX.Element {
render(): JSX.Element { const message = sanitizeHtml(
const { output, defaultOutput } = this.props; !isEmpty(output) ? output.join('\n') : defaultOutput,
const message = sanitizeHtml( {
!isEmpty(output) ? output.join('\n') : defaultOutput, allowedTags: ['b', 'i', 'em', 'strong', 'code', 'wbr']
{ }
allowedTags: ['b', 'i', 'em', 'strong', 'code', 'wbr'] );
} return (
); <pre
return ( className='output-text'
<pre dangerouslySetInnerHTML={{ __html: message }}
className='output-text' />
dangerouslySetInnerHTML={{ __html: message }} );
/>
);
}
} }
export default Output; export default Output;

View File

@ -1,36 +1,28 @@
import Prism from 'prismjs'; import Prism from 'prismjs';
import React, { Component } from 'react'; import React, { useRef, useEffect } from 'react';
interface PrismFormattedProps { interface PrismFormattedProps {
className?: string; className?: string;
text: string; text: string;
} }
class PrismFormatted extends Component<PrismFormattedProps> { function PrismFormatted({ className, text }: PrismFormattedProps): JSX.Element {
static displayName: string; const instructionsRef = useRef<HTMLDivElement>(null);
instructionsRef: React.RefObject<HTMLInputElement>;
componentDidMount(): void { useEffect(() => {
// Just in case 'current' has not been created, though it should have been. // Just in case 'current' has not been created, though it should have been.
if (this.instructionsRef.current) { if (instructionsRef.current) {
Prism.highlightAllUnder(this.instructionsRef.current); Prism.highlightAllUnder(instructionsRef.current);
} }
} }, []);
constructor(props: PrismFormattedProps | Readonly<PrismFormattedProps>) { return (
super(props); <div
this.instructionsRef = React.createRef(); className={className}
} dangerouslySetInnerHTML={{ __html: text }}
ref={instructionsRef}
render(): JSX.Element { />
const { text, className } = this.props; );
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: text }}
ref={this.instructionsRef}
/>
);
}
} }
PrismFormatted.displayName = 'PrismFormatted'; PrismFormatted.displayName = 'PrismFormatted';

View File

@ -1,5 +1,5 @@
import { Button } from '@freecodecamp/react-bootstrap'; import { Button } from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react'; import React from 'react';
import { TFunction, withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
@ -24,34 +24,34 @@ interface ToolPanelProps {
t: TFunction; t: TFunction;
} }
export class ToolPanel extends Component<ToolPanelProps> { export function ToolPanel({
static displayName: string; guideUrl,
render(): JSX.Element { openHelpModal,
const { guideUrl, openHelpModal, t } = this.props; t
return ( }: ToolPanelProps): JSX.Element {
<div className='tool-panel-group project-tool-panel'> return (
{guideUrl && ( <div className='tool-panel-group project-tool-panel'>
<Button {guideUrl && (
block={true}
bsStyle='primary'
className='btn-invert'
href={guideUrl}
target='_blank'
>
{t('buttons.get-hint')}
</Button>
)}
<Button <Button
block={true} block={true}
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
onClick={openHelpModal} href={guideUrl}
target='_blank'
> >
{t('buttons.ask-for-help')} {t('buttons.get-hint')}
</Button> </Button>
</div> )}
); <Button
} block={true}
bsStyle='primary'
className='btn-invert'
onClick={openHelpModal}
>
{t('buttons.ask-for-help')}
</Button>
</div>
);
} }
ToolPanel.displayName = 'ProjectToolPanel'; ToolPanel.displayName = 'ProjectToolPanel';