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,
Row
} from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react';
import React, { useState } from 'react';
import Helmet from 'react-helmet';
import { TFunction, Trans, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
@ -58,111 +58,94 @@ const mapDispatchToProps = {
reportUser
};
class ShowUser extends Component<IShowUserProps> {
state: {
textarea: string;
};
constructor(props: IShowUserProps) {
super(props);
function ShowUser({
email,
isSignedIn,
reportUser,
t,
userFetchState,
username
}: IShowUserProps) {
const [textarea, setTextarea] = useState('');
this.state = {
textarea: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
setTextarea(e.target.value.slice());
}
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
const textarea = e.target.value.slice();
return this.setState({
textarea
});
}
handleSubmit(e: React.FormEvent) {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const { textarea: reportDescription } = this.state;
const { username, reportUser } = this.props;
return reportUser({ username, reportDescription });
reportUser({ username, reportDescription: textarea });
}
render() {
const { username, isSignedIn, userFetchState, email, t } = this.props;
const { pending, complete, errored } = userFetchState;
if (pending && !complete) {
return <Loader fullScreen={true} />;
}
const { pending, complete, errored } = userFetchState;
if (pending && !complete) {
return <Loader fullScreen={true} />;
}
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');
if ((complete || errored) && !isSignedIn) {
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>
{/* eslint-disable @typescript-eslint/unbound-method */}
<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>
</>
<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>
);
}
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';
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 { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -52,48 +52,46 @@ type LearnLayoutProps = {
children?: React.ReactNode;
};
class LearnLayout extends Component<LearnLayoutProps> {
static displayName = 'LearnLayout';
function LearnLayout({
isSignedIn,
fetchState,
user,
tryToShowDonationModal,
children
}: LearnLayoutProps): JSX.Element {
useEffect(() => {
tryToShowDonationModal();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
componentDidMount() {
this.props.tryToShowDonationModal();
useEffect(() => {
return () => {
const metaTag = document.querySelector(`meta[name="robots"]`);
if (metaTag) {
metaTag.remove();
}
};
}, []);
if (fetchState.pending && !fetchState.complete) {
return <Loader fullScreen={true} />;
}
componentWillUnmount() {
const metaTag = document.querySelector(`meta[name="robots"]`);
if (metaTag) {
metaTag.remove();
}
if (isSignedIn && !user.acceptedPrivacyTerms) {
return <RedirectEmailSignUp />;
}
render() {
const {
fetchState: { pending, complete },
isSignedIn,
user: { acceptedPrivacyTerms },
children
} = this.props;
if (pending && !complete) {
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 />
</>
);
}
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);

View File

@ -9,7 +9,7 @@ import {
import Loadable from '@loadable/component';
import { useStaticQuery, graphql } from 'gatsby';
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 envData from '../../../../../config/env.json';
@ -66,43 +66,58 @@ interface TimelineInnerProps extends TimelineProps {
totalPages: number;
}
interface TimeLineInnerState {
solutionToView: string | null;
solutionOpen: boolean;
pageNo: number;
solution: string | null;
challengeFiles: ChallengeFiles;
}
function TimelineInner({
idToNameMap,
sortedTimeline,
totalPages,
class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
constructor(props: TimelineInnerProps) {
super(props);
completedMap,
t,
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 = {
solutionToView: null,
solutionOpen: false,
pageNo: 1,
solution: null,
challengeFiles: null
};
this.closeSolution = this.closeSolution.bind(this);
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);
function viewSolution(
id: string,
solution_: string,
challengeFiles_: ChallengeFiles
): void {
setSolutionToView(id);
setSolutionOpen(true);
setSolution(solution_);
setChallengeFiles(challengeFiles_);
}
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,
challengeFiles: ChallengeFiles,
githubLink: string,
solution: string
): React.ReactNode {
const { t } = this.props;
if (challengeFiles?.length) {
return (
<Button
@ -110,7 +125,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
bsStyle='primary'
className='btn-invert'
id={`btn-for-${id}`}
onClick={() => this.viewSolution(id, solution, challengeFiles)}
onClick={() => viewSolution(id, solution, challengeFiles)}
>
{t('buttons.show-code')}
</Button>
@ -163,8 +178,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
}
}
renderCompletion(completed: SortedTimeline): JSX.Element {
const { idToNameMap, username } = this.props;
function renderCompletion(completed: SortedTimeline): JSX.Element {
const { id, challengeFiles, githubLink, solution } = completed;
const completedDate = new Date(completed.completedDate);
// @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>
)}
</td>
<td>
{this.renderViewButton(id, challengeFiles, githubLink, solution)}
</td>
<td>{renderViewButton(id, challengeFiles, githubLink, solution)}</td>
<td className='text-center'>
<time dateTime={completedDate.toISOString()}>
{completedDate.toLocaleString([localeCode, 'en-US'], {
@ -199,127 +211,72 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
</tr>
);
}
viewSolution(
id: string,
solution: string,
challengeFiles: ChallengeFiles
): void {
this.setState(state => ({
...state,
solutionToView: id,
solutionOpen: true,
solution,
challengeFiles
}));
}
closeSolution() {
this.setState(state => ({
...state,
solutionToView: null,
solutionOpen: false,
solution: null,
challengeFiles: null
}));
}
const id = solutionToView;
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
const endIndex = pageNo * ITEMS_PER_PAGE;
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 endIndex = pageNo * ITEMS_PER_PAGE;
return (
<FullWidthRow>
<h2 className='text-center'>{t('profile.timeline')}</h2>
{completedMap.length === 0 ? (
<p className='text-center'>
{t('profile.none-completed')}&nbsp;
<Link to='/learn'>{t('profile.get-started')}</Link>
</p>
) : (
<Table condensed={true} striped={true}>
<thead>
<tr>
<th>{t('profile.challenge')}</th>
<th>{t('settings.labels.solution')}</th>
<th className='text-center'>{t('profile.completed')}</th>
</tr>
</thead>
<tbody>
{sortedTimeline
.slice(startIndex, endIndex)
.map(this.renderCompletion)}
</tbody>
</Table>
)}
{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>
);
}
return (
<FullWidthRow>
<h2 className='text-center'>{t('profile.timeline')}</h2>
{completedMap.length === 0 ? (
<p className='text-center'>
{t('profile.none-completed')}&nbsp;
<Link to='/learn'>{t('profile.get-started')}</Link>
</p>
) : (
<Table condensed={true} striped={true}>
<thead>
<tr>
<th>{t('profile.challenge')}</th>
<th>{t('settings.labels.solution')}</th>
<th className='text-center'>{t('profile.completed')}</th>
</tr>
</thead>
<tbody>
{sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
</tbody>
</Table>
)}
{id && (
<Modal
aria-labelledby='contained-modal-title'
onHide={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={challengeFiles}
solution={solution ?? ''}
/>
</Modal.Body>
<Modal.Footer>
<Button onClick={closeSolution}>{t('buttons.close')}</Button>
</Modal.Footer>
</Modal>
)}
{totalPages > 1 && (
<TimelinePagination
firstPage={firstPage}
lastPage={lastPage}
nextPage={nextPage}
pageNo={pageNo}
prevPage={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*/
function useIdToNameMap(): Map<string, string> {
const {

View File

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

View File

@ -7,7 +7,7 @@ import {
Button
} from '@freecodecamp/react-bootstrap';
import { Link } from 'gatsby';
import React, { Component } from 'react';
import React, { useState } from 'react';
import { TFunction, Trans, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -36,70 +36,49 @@ type EmailProps = {
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
};
type EmailState = {
emailForm: {
currentEmail: string;
newEmail: string;
confirmNewEmail: string;
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>
);
interface EmailForm {
currentEmail: string;
newEmail: string;
confirmNewEmail: string;
isPristine: boolean;
}
class EmailSettings extends Component<EmailProps, EmailState> {
static displayName: string;
constructor(props: EmailProps) {
super(props);
function EmailSettings({
email,
isEmailVerified,
sendQuincyEmail,
t,
updateMyEmail,
updateQuincyEmail
}: EmailProps): JSX.Element {
const [emailForm, setEmailForm] = useState<EmailForm>({
currentEmail: email,
newEmail: '',
confirmNewEmail: '',
isPristine: true
});
this.state = {
emailForm: {
currentEmail: props.email,
newEmail: '',
confirmNewEmail: '',
isPristine: true
}
function handleSubmit(e: React.FormEvent): void {
e.preventDefault();
updateMyEmail(emailForm.newEmail);
}
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) => {
e.preventDefault();
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;
function getValidationForNewEmail() {
const { newEmail, currentEmail } = emailForm;
if (!maybeEmailRE.test(newEmail)) {
return {
state: null,
@ -120,14 +99,10 @@ class EmailSettings extends Component<EmailProps, EmailState> {
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)) {
return {
state: null,
@ -146,115 +121,109 @@ class EmailSettings extends Component<EmailProps, EmailState> {
message: ''
};
}
};
}
render() {
const {
emailForm: { newEmail, confirmNewEmail, currentEmail, isPristine }
} = this.state;
const { newEmail, confirmNewEmail, currentEmail, isPristine } = emailForm;
const { isEmailVerified, updateQuincyEmail, sendQuincyEmail, t } =
this.props;
const { state: newEmailValidation, message: newEmailValidationMessage } =
getValidationForNewEmail();
const { state: newEmailValidation, message: newEmailValidationMessage } =
this.getValidationForNewEmail();
const {
state: confirmEmailValidation,
message: confirmEmailValidationMessage
} = getValidationForConfirmEmail();
const {
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>
);
}
if (!currentEmail) {
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>
)}
<div>
<FullWidthRow>
<form id='form-update-email' onSubmit={this.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={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>
<p className='large-p text-center'>{t('settings.email.missing')}</p>
</FullWidthRow>
<Spacer />
<FullWidthRow>
<form id='form-quincy-email' onSubmit={this.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>
<Link style={{ textDecoration: 'none' }} to='/update-email'>
<Button block={true} bsSize='lg' bsStyle='primary'>
{t('buttons.edit')}
</Button>
</Link>
</FullWidthRow>
</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';

View File

@ -1,5 +1,5 @@
import { Button, Form } from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react';
import React from 'react';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -44,142 +44,145 @@ type PrivacyProps = {
};
};
class PrivacySettings extends Component<PrivacyProps> {
static displayName: string;
handleSubmit = (e: React.FormEvent) => e.preventDefault();
toggleFlag = (flag: string) => () => {
const privacyValues = { ...this.props.user.profileUI };
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 PrivacySettings({
submitProfileUI,
t,
user
}: PrivacyProps): JSX.Element {
function handleSubmit(e: React.FormEvent): void {
e.preventDefault();
}
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';

View File

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

View File

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

View File

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

View File

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