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,36 +58,25 @@ 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 { username, isSignedIn, userFetchState, email, t } = this.props;
const { pending, complete, errored } = userFetchState; const { pending, complete, errored } = userFetchState;
if (pending && !complete) { if (pending && !complete) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
@ -117,8 +106,6 @@ class ShowUser extends Component<IShowUserProps> {
); );
} }
const { textarea } = this.state;
const placeholderText = t('report.details');
return ( return (
<> <>
<Helmet> <Helmet>
@ -138,14 +125,13 @@ class ShowUser extends Component<IShowUserProps> {
</Trans> </Trans>
</p> </p>
<p>{t('report.notify-2')}</p> <p>{t('report.notify-2')}</p>
{/* eslint-disable @typescript-eslint/unbound-method */} <form onSubmit={handleSubmit}>
<form onSubmit={this.handleSubmit}>
<FormGroup controlId='report-user-textarea'> <FormGroup controlId='report-user-textarea'>
<ControlLabel>{t('report.what')}</ControlLabel> <ControlLabel>{t('report.what')}</ControlLabel>
<FormControl <FormControl
componentClass='textarea' componentClass='textarea'
onChange={this.handleChange} onChange={handleChange}
placeholder={placeholderText} placeholder={t('report.details')}
value={textarea} value={textarea}
/> />
</FormGroup> </FormGroup>
@ -154,15 +140,12 @@ class ShowUser extends Component<IShowUserProps> {
</Button> </Button>
<Spacer /> <Spacer />
</form> </form>
{/* eslint-disable @typescript-eslint/unbound-method */}
</Col> </Col>
</Row> </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,33 +52,32 @@ 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 () => {
}
componentWillUnmount() {
const metaTag = document.querySelector(`meta[name="robots"]`); const metaTag = document.querySelector(`meta[name="robots"]`);
if (metaTag) { if (metaTag) {
metaTag.remove(); metaTag.remove();
} }
} };
}, []);
render() { if (fetchState.pending && !fetchState.complete) {
const {
fetchState: { pending, complete },
isSignedIn,
user: { acceptedPrivacyTerms },
children
} = this.props;
if (pending && !complete) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
} }
if (isSignedIn && !acceptedPrivacyTerms) { if (isSignedIn && !user.acceptedPrivacyTerms) {
return <RedirectEmailSignUp />; return <RedirectEmailSignUp />;
} }
@ -93,7 +92,6 @@ class LearnLayout extends Component<LearnLayoutProps> {
<DonateModal /> <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,61 +211,8 @@ 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 => ({
...state,
solutionToView: null,
solutionOpen: false,
solution: null,
challengeFiles: null
}));
}
firstPage() {
this.setState({
pageNo: 1
});
}
nextPage() {
this.setState(state => ({
pageNo: state.pageNo + 1
}));
}
prevPage() {
this.setState(state => ({
pageNo: state.pageNo - 1
}));
}
lastPage() {
this.setState((_, props) => ({
pageNo: props.totalPages
}));
}
render() {
const {
completedMap,
idToNameMap,
username,
sortedTimeline,
t,
totalPages = 1
} = this.props;
const { solutionToView: id, solutionOpen, pageNo = 1 } = this.state;
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE; const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
const endIndex = pageNo * ITEMS_PER_PAGE; const endIndex = pageNo * ITEMS_PER_PAGE;
@ -275,16 +234,14 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sortedTimeline {sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
.slice(startIndex, endIndex)
.map(this.renderCompletion)}
</tbody> </tbody>
</Table> </Table>
)} )}
{id && ( {id && (
<Modal <Modal
aria-labelledby='contained-modal-title' aria-labelledby='contained-modal-title'
onHide={this.closeSolution} onHide={closeSolution}
show={solutionOpen} show={solutionOpen}
> >
<Modal.Header closeButton={true}> <Modal.Header closeButton={true}>
@ -297,29 +254,29 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<SolutionViewer <SolutionViewer
challengeFiles={this.state.challengeFiles} challengeFiles={challengeFiles}
solution={this.state.solution ?? ''} solution={solution ?? ''}
/> />
</Modal.Body> </Modal.Body>
<Modal.Footer> <Modal.Footer>
<Button onClick={this.closeSolution}>{t('buttons.close')}</Button> <Button onClick={closeSolution}>{t('buttons.close')}</Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
)} )}
{totalPages > 1 && ( {totalPages > 1 && (
<TimelinePagination <TimelinePagination
firstPage={this.firstPage} firstPage={firstPage}
lastPage={this.lastPage} lastPage={lastPage}
nextPage={this.nextPage} nextPage={nextPage}
pageNo={pageNo} pageNo={pageNo}
prevPage={this.prevPage} prevPage={prevPage}
totalPages={totalPages} totalPages={totalPages}
/> />
)} )}
</FullWidthRow> </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,32 +28,19 @@ 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 this.setState(state => ({
...state,
delete: !state.delete
}));
};
render() {
const { deleteAccount, resetProgress, t } = this.props;
return ( return (
<div className='danger-zone text-center'> <div className='danger-zone text-center'>
<FullWidthRow> <FullWidthRow>
@ -72,7 +54,7 @@ class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
bsSize='lg' bsSize='lg'
bsStyle='danger' bsStyle='danger'
className='btn-danger' className='btn-danger'
onClick={() => this.toggleResetModal()} onClick={toggleResetModal}
type='button' type='button'
> >
{t('settings.danger.reset')} {t('settings.danger.reset')}
@ -83,7 +65,7 @@ class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
bsSize='lg' bsSize='lg'
bsStyle='danger' bsStyle='danger'
className='btn-danger' className='btn-danger'
onClick={() => this.toggleDeleteModal()} onClick={toggleDeleteModal}
type='button' type='button'
> >
{t('settings.danger.delete')} {t('settings.danger.delete')}
@ -93,19 +75,18 @@ class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
</Panel> </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,
this.state = { updateMyEmail,
emailForm: { updateQuincyEmail
currentEmail: props.email, }: EmailProps): JSX.Element {
const [emailForm, setEmailForm] = useState<EmailForm>({
currentEmail: email,
newEmail: '', newEmail: '',
confirmNewEmail: '', confirmNewEmail: '',
isPristine: true isPristine: true
} });
};
function handleSubmit(e: React.FormEvent): void {
e.preventDefault();
updateMyEmail(emailForm.newEmail);
} }
handleSubmit = (e: React.FormEvent) => { function createHandleEmailFormChange(
key: 'newEmail' | 'confirmNewEmail'
): (e: React.ChangeEvent<HTMLInputElement>) => void {
return e => {
e.preventDefault(); e.preventDefault();
const { const userInput = e.target.value.slice();
emailForm: { newEmail } setEmailForm(prev => ({
} = this.state; ...prev,
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, [key]: userInput,
isPristine: userInput === state.emailForm.currentEmail isPristine: userInput === prev.currentEmail
}
})); }));
}; };
}
getValidationForNewEmail = () => { function getValidationForNewEmail() {
const { const { newEmail, currentEmail } = emailForm;
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,23 +121,17 @@ 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 } =
this.props;
const { state: newEmailValidation, message: newEmailValidationMessage } = const { state: newEmailValidation, message: newEmailValidationMessage } =
this.getValidationForNewEmail(); getValidationForNewEmail();
const { const {
state: confirmEmailValidation, state: confirmEmailValidation,
message: confirmEmailValidationMessage message: confirmEmailValidationMessage
} = this.getValidationForConfirmEmail(); } = getValidationForConfirmEmail();
if (!currentEmail) { if (!currentEmail) {
return ( return (
@ -171,7 +140,11 @@ class EmailSettings extends Component<EmailProps, EmailState> {
<p className='large-p text-center'>{t('settings.email.missing')}</p> <p className='large-p text-center'>{t('settings.email.missing')}</p>
</FullWidthRow> </FullWidthRow>
<FullWidthRow> <FullWidthRow>
<UpdateEmailButton /> <Link style={{ textDecoration: 'none' }} to='/update-email'>
<Button block={true} bsSize='lg' bsStyle='primary'>
{t('buttons.edit')}
</Button>
</Link>
</FullWidthRow> </FullWidthRow>
</div> </div>
); );
@ -197,18 +170,15 @@ class EmailSettings extends Component<EmailProps, EmailState> {
</FullWidthRow> </FullWidthRow>
)} )}
<FullWidthRow> <FullWidthRow>
<form id='form-update-email' onSubmit={this.handleSubmit}> <form id='form-update-email' onSubmit={handleSubmit}>
<FormGroup controlId='current-email'> <FormGroup controlId='current-email'>
<ControlLabel>{t('settings.email.current')}</ControlLabel> <ControlLabel>{t('settings.email.current')}</ControlLabel>
<FormControl.Static>{currentEmail}</FormControl.Static> <FormControl.Static>{currentEmail}</FormControl.Static>
</FormGroup> </FormGroup>
<FormGroup <FormGroup controlId='new-email' validationState={newEmailValidation}>
controlId='new-email'
validationState={newEmailValidation}
>
<ControlLabel>{t('settings.email.new')}</ControlLabel> <ControlLabel>{t('settings.email.new')}</ControlLabel>
<FormControl <FormControl
onChange={this.createHandleEmailFormChange('newEmail')} onChange={createHandleEmailFormChange('newEmail')}
type='email' type='email'
value={newEmail} value={newEmail}
/> />
@ -222,7 +192,7 @@ class EmailSettings extends Component<EmailProps, EmailState> {
> >
<ControlLabel>{t('settings.email.confirm')}</ControlLabel> <ControlLabel>{t('settings.email.confirm')}</ControlLabel>
<FormControl <FormControl
onChange={this.createHandleEmailFormChange('confirmNewEmail')} onChange={createHandleEmailFormChange('confirmNewEmail')}
type='email' type='email'
value={confirmNewEmail} value={confirmNewEmail}
/> />
@ -241,7 +211,7 @@ class EmailSettings extends Component<EmailProps, EmailState> {
</FullWidthRow> </FullWidthRow>
<Spacer /> <Spacer />
<FullWidthRow> <FullWidthRow>
<form id='form-quincy-email' onSubmit={this.handleSubmit}> <form id='form-quincy-email' onSubmit={handleSubmit}>
<ToggleSetting <ToggleSetting
action={t('settings.email.weekly')} action={t('settings.email.weekly')}
flag={sendQuincyEmail} flag={sendQuincyEmail}
@ -254,7 +224,6 @@ class EmailSettings extends Component<EmailProps, EmailState> {
</FullWidthRow> </FullWidthRow>
</div> </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,20 +44,24 @@ type PrivacyProps = {
}; };
}; };
class PrivacySettings extends Component<PrivacyProps> { function PrivacySettings({
static displayName: string; submitProfileUI,
t,
user
}: PrivacyProps): JSX.Element {
function handleSubmit(e: React.FormEvent): void {
e.preventDefault();
}
handleSubmit = (e: React.FormEvent) => e.preventDefault(); function toggleFlag(flag: string): () => void {
return () => {
toggleFlag = (flag: string) => () => { const privacyValues = { ...user.profileUI };
const privacyValues = { ...this.props.user.profileUI };
privacyValues[flag as keyof ProfileUIType] = privacyValues[flag as keyof ProfileUIType] =
!privacyValues[flag as keyof ProfileUIType]; !privacyValues[flag as keyof ProfileUIType];
this.props.submitProfileUI(privacyValues); submitProfileUI(privacyValues);
}; };
}
render() {
const { t, user } = this.props;
const { const {
isLocked = true, isLocked = true,
showAbout = false, showAbout = false,
@ -76,7 +80,7 @@ class PrivacySettings extends Component<PrivacyProps> {
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader> <SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
<FullWidthRow> <FullWidthRow>
<p>{t('settings.privacy')}</p> <p>{t('settings.privacy')}</p>
<Form inline={true} onSubmit={this.handleSubmit}> <Form inline={true} onSubmit={handleSubmit}>
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-profile')} action={t('settings.labels.my-profile')}
explain={t('settings.disabled')} explain={t('settings.disabled')}
@ -84,7 +88,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='isLocked' flagName='isLocked'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('isLocked')} toggleFlag={toggleFlag('isLocked')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-name')} action={t('settings.labels.my-name')}
@ -93,7 +97,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='name' flagName='name'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showName')} toggleFlag={toggleFlag('showName')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-location')} action={t('settings.labels.my-location')}
@ -101,7 +105,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showLocation' flagName='showLocation'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showLocation')} toggleFlag={toggleFlag('showLocation')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-about')} action={t('settings.labels.my-about')}
@ -109,7 +113,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showAbout' flagName='showAbout'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showAbout')} toggleFlag={toggleFlag('showAbout')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-points')} action={t('settings.labels.my-points')}
@ -117,7 +121,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showPoints' flagName='showPoints'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showPoints')} toggleFlag={toggleFlag('showPoints')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-heatmap')} action={t('settings.labels.my-heatmap')}
@ -125,7 +129,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showHeatMap' flagName='showHeatMap'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showHeatMap')} toggleFlag={toggleFlag('showHeatMap')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-certs')} action={t('settings.labels.my-certs')}
@ -134,7 +138,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showCerts' flagName='showCerts'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showCerts')} toggleFlag={toggleFlag('showCerts')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-portfolio')} action={t('settings.labels.my-portfolio')}
@ -142,7 +146,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showPortfolio' flagName='showPortfolio'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showPortfolio')} toggleFlag={toggleFlag('showPortfolio')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-timeline')} action={t('settings.labels.my-timeline')}
@ -150,7 +154,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showTimeLine' flagName='showTimeLine'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showTimeLine')} toggleFlag={toggleFlag('showTimeLine')}
/> />
<ToggleSetting <ToggleSetting
action={t('settings.labels.my-donations')} action={t('settings.labels.my-donations')}
@ -158,7 +162,7 @@ class PrivacySettings extends Component<PrivacyProps> {
flagName='showPortfolio' flagName='showPortfolio'
offLabel={t('buttons.public')} offLabel={t('buttons.public')}
onLabel={t('buttons.private')} onLabel={t('buttons.private')}
toggleFlag={this.toggleFlag('showDonation')} toggleFlag={toggleFlag('showDonation')}
/> />
</Form> </Form>
</FullWidthRow> </FullWidthRow>
@ -179,7 +183,6 @@ class PrivacySettings extends Component<PrivacyProps> {
</FullWidthRow> </FullWidthRow>
</div> </div>
); );
}
} }
PrivacySettings.displayName = 'PrivacySettings'; PrivacySettings.displayName = 'PrivacySettings';

View File

@ -9,10 +9,16 @@ interface HTMLProps {
preBodyComponents?: React.ReactNode[]; preBodyComponents?: React.ReactNode[];
} }
export default class HTML extends React.Component<HTMLProps> { export default function HTML({
render(): JSX.Element { body,
bodyAttributes,
headComponents,
htmlAttributes,
postBodyComponents,
preBodyComponents
}: HTMLProps): JSX.Element {
return ( return (
<html id='__fcc-html' {...this.props.htmlAttributes} lang='en'> <html id='__fcc-html' {...htmlAttributes} lang='en'>
<head> <head>
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<meta content='ie=edge' httpEquiv='x-ua-compatible' /> <meta content='ie=edge' httpEquiv='x-ua-compatible' />
@ -20,19 +26,18 @@ export default class HTML extends React.Component<HTMLProps> {
content='width=device-width, initial-scale=1.0, shrink-to-fit=no' content='width=device-width, initial-scale=1.0, shrink-to-fit=no'
name='viewport' name='viewport'
/> />
{this.props.headComponents} {headComponents}
</head> </head>
<body {...this.props.bodyAttributes}> <body {...bodyAttributes}>
{this.props.preBodyComponents} {preBodyComponents}
<div <div
className='tex2jax_ignore' className='tex2jax_ignore'
dangerouslySetInnerHTML={{ __html: this.props.body }} dangerouslySetInnerHTML={{ __html: body }}
id='___gatsby' id='___gatsby'
key={'body'} key={'body'}
/> />
{this.props.postBodyComponents} {postBodyComponents}
</body> </body>
</html> </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,9 +9,7 @@ interface OutputProps {
output: string[]; output: string[];
} }
class Output extends Component<OutputProps> { function Output({ defaultOutput, output }: OutputProps): JSX.Element {
render(): JSX.Element {
const { output, defaultOutput } = this.props;
const message = sanitizeHtml( const message = sanitizeHtml(
!isEmpty(output) ? output.join('\n') : defaultOutput, !isEmpty(output) ? output.join('\n') : defaultOutput,
{ {
@ -24,7 +22,6 @@ class Output extends Component<OutputProps> {
dangerouslySetInnerHTML={{ __html: message }} 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>) {
super(props);
this.instructionsRef = React.createRef();
}
render(): JSX.Element {
const { text, className } = this.props;
return ( return (
<div <div
className={className} className={className}
dangerouslySetInnerHTML={{ __html: text }} dangerouslySetInnerHTML={{ __html: text }}
ref={this.instructionsRef} ref={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,10 +24,11 @@ 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
}: ToolPanelProps): JSX.Element {
return ( return (
<div className='tool-panel-group project-tool-panel'> <div className='tool-panel-group project-tool-panel'>
{guideUrl && ( {guideUrl && (
@ -51,7 +52,6 @@ export class ToolPanel extends Component<ToolPanelProps> {
</Button> </Button>
</div> </div>
); );
}
} }
ToolPanel.displayName = 'ProjectToolPanel'; ToolPanel.displayName = 'ProjectToolPanel';