feat(client): convert class components to functional components (#43226)
This commit is contained in:
@ -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()(
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
Reference in New Issue
Block a user