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,111 +58,94 @@ const mapDispatchToProps = {
|
|||||||
reportUser
|
reportUser
|
||||||
};
|
};
|
||||||
|
|
||||||
class ShowUser extends Component<IShowUserProps> {
|
function ShowUser({
|
||||||
state: {
|
email,
|
||||||
textarea: string;
|
isSignedIn,
|
||||||
};
|
reportUser,
|
||||||
constructor(props: IShowUserProps) {
|
t,
|
||||||
super(props);
|
userFetchState,
|
||||||
|
username
|
||||||
|
}: IShowUserProps) {
|
||||||
|
const [textarea, setTextarea] = useState('');
|
||||||
|
|
||||||
this.state = {
|
function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
textarea: ''
|
setTextarea(e.target.value.slice());
|
||||||
};
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
function handleSubmit(e: React.FormEvent) {
|
||||||
const textarea = e.target.value.slice();
|
|
||||||
return this.setState({
|
|
||||||
textarea
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { textarea: reportDescription } = this.state;
|
reportUser({ username, reportDescription: textarea });
|
||||||
const { username, reportUser } = this.props;
|
|
||||||
return reportUser({ username, reportDescription });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const { pending, complete, errored } = userFetchState;
|
||||||
const { username, isSignedIn, userFetchState, email, t } = this.props;
|
if (pending && !complete) {
|
||||||
const { pending, complete, errored } = userFetchState;
|
return <Loader fullScreen={true} />;
|
||||||
if (pending && !complete) {
|
}
|
||||||
return <Loader fullScreen={true} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((complete || errored) && !isSignedIn) {
|
if ((complete || errored) && !isSignedIn) {
|
||||||
return (
|
|
||||||
<main>
|
|
||||||
<FullWidthRow>
|
|
||||||
<Spacer size={2} />
|
|
||||||
<Panel bsStyle='info' className='text-center'>
|
|
||||||
<Panel.Heading>
|
|
||||||
<Panel.Title componentClass='h3'>
|
|
||||||
{t('report.sign-in')}
|
|
||||||
</Panel.Title>
|
|
||||||
</Panel.Heading>
|
|
||||||
<Panel.Body className='text-center'>
|
|
||||||
<Spacer size={2} />
|
|
||||||
<Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
|
|
||||||
<Login block={true}>{t('buttons.click-here')}</Login>
|
|
||||||
</Col>
|
|
||||||
<Spacer size={3} />
|
|
||||||
</Panel.Body>
|
|
||||||
</Panel>
|
|
||||||
</FullWidthRow>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { textarea } = this.state;
|
|
||||||
const placeholderText = t('report.details');
|
|
||||||
return (
|
return (
|
||||||
<>
|
<main>
|
||||||
<Helmet>
|
<FullWidthRow>
|
||||||
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
|
<Spacer size={2} />
|
||||||
</Helmet>
|
<Panel bsStyle='info' className='text-center'>
|
||||||
<Spacer size={2} />
|
<Panel.Heading>
|
||||||
<Row className='text-center overflow-fix'>
|
<Panel.Title componentClass='h3'>
|
||||||
<Col sm={8} smOffset={2} xs={12}>
|
{t('report.sign-in')}
|
||||||
<h2>{t('report.portfolio-2', { username: username })}</h2>
|
</Panel.Title>
|
||||||
</Col>
|
</Panel.Heading>
|
||||||
</Row>
|
<Panel.Body className='text-center'>
|
||||||
<Row className='overflow-fix'>
|
<Spacer size={2} />
|
||||||
<Col sm={6} smOffset={3} xs={12}>
|
<Col md={6} mdOffset={3} sm={8} smOffset={2} xs={12}>
|
||||||
<p>
|
<Login block={true}>{t('buttons.click-here')}</Login>
|
||||||
<Trans i18nKey='report.notify-1'>
|
</Col>
|
||||||
<strong>{{ email }}</strong>
|
<Spacer size={3} />
|
||||||
</Trans>
|
</Panel.Body>
|
||||||
</p>
|
</Panel>
|
||||||
<p>{t('report.notify-2')}</p>
|
</FullWidthRow>
|
||||||
{/* eslint-disable @typescript-eslint/unbound-method */}
|
</main>
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<FormGroup controlId='report-user-textarea'>
|
|
||||||
<ControlLabel>{t('report.what')}</ControlLabel>
|
|
||||||
<FormControl
|
|
||||||
componentClass='textarea'
|
|
||||||
onChange={this.handleChange}
|
|
||||||
placeholder={placeholderText}
|
|
||||||
value={textarea}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<Button block={true} bsStyle='primary' type='submit'>
|
|
||||||
{t('report.submit')}
|
|
||||||
</Button>
|
|
||||||
<Spacer />
|
|
||||||
</form>
|
|
||||||
{/* eslint-disable @typescript-eslint/unbound-method */}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
|
||||||
|
</Helmet>
|
||||||
|
<Spacer size={2} />
|
||||||
|
<Row className='text-center overflow-fix'>
|
||||||
|
<Col sm={8} smOffset={2} xs={12}>
|
||||||
|
<h2>{t('report.portfolio-2', { username: username })}</h2>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className='overflow-fix'>
|
||||||
|
<Col sm={6} smOffset={3} xs={12}>
|
||||||
|
<p>
|
||||||
|
<Trans i18nKey='report.notify-1'>
|
||||||
|
<strong>{{ email }}</strong>
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
<p>{t('report.notify-2')}</p>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<FormGroup controlId='report-user-textarea'>
|
||||||
|
<ControlLabel>{t('report.what')}</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
componentClass='textarea'
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={t('report.details')}
|
||||||
|
value={textarea}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<Button block={true} bsStyle='primary' type='submit'>
|
||||||
|
{t('report.submit')}
|
||||||
|
</Button>
|
||||||
|
<Spacer />
|
||||||
|
</form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error Config might need to be remedied, or component transformed into F.C.
|
|
||||||
ShowUser.displayName = 'ShowUser';
|
ShowUser.displayName = 'ShowUser';
|
||||||
|
|
||||||
export default withTranslation()(
|
export default withTranslation()(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
@ -52,48 +52,46 @@ type LearnLayoutProps = {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LearnLayout extends Component<LearnLayoutProps> {
|
function LearnLayout({
|
||||||
static displayName = 'LearnLayout';
|
isSignedIn,
|
||||||
|
fetchState,
|
||||||
|
user,
|
||||||
|
tryToShowDonationModal,
|
||||||
|
children
|
||||||
|
}: LearnLayoutProps): JSX.Element {
|
||||||
|
useEffect(() => {
|
||||||
|
tryToShowDonationModal();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
this.props.tryToShowDonationModal();
|
return () => {
|
||||||
|
const metaTag = document.querySelector(`meta[name="robots"]`);
|
||||||
|
if (metaTag) {
|
||||||
|
metaTag.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (fetchState.pending && !fetchState.complete) {
|
||||||
|
return <Loader fullScreen={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
if (isSignedIn && !user.acceptedPrivacyTerms) {
|
||||||
const metaTag = document.querySelector(`meta[name="robots"]`);
|
return <RedirectEmailSignUp />;
|
||||||
if (metaTag) {
|
|
||||||
metaTag.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const {
|
<>
|
||||||
fetchState: { pending, complete },
|
<Helmet>
|
||||||
isSignedIn,
|
<meta content='noindex' name='robots' />
|
||||||
user: { acceptedPrivacyTerms },
|
</Helmet>
|
||||||
children
|
<main id='learn-app-wrapper'>{children}</main>
|
||||||
} = this.props;
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||||
|
/* @ts-ignore */}
|
||||||
if (pending && !complete) {
|
<DonateModal />
|
||||||
return <Loader fullScreen={true} />;
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
if (isSignedIn && !acceptedPrivacyTerms) {
|
|
||||||
return <RedirectEmailSignUp />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<meta content='noindex' name='robots' />
|
|
||||||
</Helmet>
|
|
||||||
<main id='learn-app-wrapper'>{children}</main>
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
|
||||||
/* @ts-ignore */}
|
|
||||||
<DonateModal />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(LearnLayout);
|
export default connect(mapStateToProps, mapDispatchToProps)(LearnLayout);
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import Loadable from '@loadable/component';
|
import Loadable from '@loadable/component';
|
||||||
import { useStaticQuery, graphql } from 'gatsby';
|
import { useStaticQuery, graphql } from 'gatsby';
|
||||||
import { reverse, sortBy } from 'lodash-es';
|
import { reverse, sortBy } from 'lodash-es';
|
||||||
import React, { Component, useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { TFunction, withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import envData from '../../../../../config/env.json';
|
import envData from '../../../../../config/env.json';
|
||||||
@ -66,43 +66,58 @@ interface TimelineInnerProps extends TimelineProps {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeLineInnerState {
|
function TimelineInner({
|
||||||
solutionToView: string | null;
|
idToNameMap,
|
||||||
solutionOpen: boolean;
|
sortedTimeline,
|
||||||
pageNo: number;
|
totalPages,
|
||||||
solution: string | null;
|
|
||||||
challengeFiles: ChallengeFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
completedMap,
|
||||||
constructor(props: TimelineInnerProps) {
|
t,
|
||||||
super(props);
|
username
|
||||||
|
}: TimelineInnerProps) {
|
||||||
|
const [solutionToView, setSolutionToView] = useState<string | null>(null);
|
||||||
|
const [solutionOpen, setSolutionOpen] = useState(false);
|
||||||
|
const [pageNo, setPageNo] = useState(1);
|
||||||
|
const [solution, setSolution] = useState<string | null>(null);
|
||||||
|
const [challengeFiles, setChallengeFiles] = useState<ChallengeFiles>(null);
|
||||||
|
|
||||||
this.state = {
|
function viewSolution(
|
||||||
solutionToView: null,
|
id: string,
|
||||||
solutionOpen: false,
|
solution_: string,
|
||||||
pageNo: 1,
|
challengeFiles_: ChallengeFiles
|
||||||
solution: null,
|
): void {
|
||||||
challengeFiles: null
|
setSolutionToView(id);
|
||||||
};
|
setSolutionOpen(true);
|
||||||
|
setSolution(solution_);
|
||||||
this.closeSolution = this.closeSolution.bind(this);
|
setChallengeFiles(challengeFiles_);
|
||||||
this.renderCompletion = this.renderCompletion.bind(this);
|
|
||||||
this.viewSolution = this.viewSolution.bind(this);
|
|
||||||
this.firstPage = this.firstPage.bind(this);
|
|
||||||
this.prevPage = this.prevPage.bind(this);
|
|
||||||
this.nextPage = this.nextPage.bind(this);
|
|
||||||
this.lastPage = this.lastPage.bind(this);
|
|
||||||
this.renderViewButton = this.renderViewButton.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderViewButton(
|
function closeSolution(): void {
|
||||||
|
setSolutionToView(null);
|
||||||
|
setSolutionOpen(false);
|
||||||
|
setSolution(null);
|
||||||
|
setChallengeFiles(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPage(): void {
|
||||||
|
setPageNo(1);
|
||||||
|
}
|
||||||
|
function nextPage(): void {
|
||||||
|
setPageNo(prev => prev + 1);
|
||||||
|
}
|
||||||
|
function prevPage(): void {
|
||||||
|
setPageNo(prev => prev - 1);
|
||||||
|
}
|
||||||
|
function lastPage(): void {
|
||||||
|
setPageNo(totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderViewButton(
|
||||||
id: string,
|
id: string,
|
||||||
challengeFiles: ChallengeFiles,
|
challengeFiles: ChallengeFiles,
|
||||||
githubLink: string,
|
githubLink: string,
|
||||||
solution: string
|
solution: string
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
const { t } = this.props;
|
|
||||||
if (challengeFiles?.length) {
|
if (challengeFiles?.length) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -110,7 +125,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
|||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-invert'
|
className='btn-invert'
|
||||||
id={`btn-for-${id}`}
|
id={`btn-for-${id}`}
|
||||||
onClick={() => this.viewSolution(id, solution, challengeFiles)}
|
onClick={() => viewSolution(id, solution, challengeFiles)}
|
||||||
>
|
>
|
||||||
{t('buttons.show-code')}
|
{t('buttons.show-code')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -163,8 +178,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletion(completed: SortedTimeline): JSX.Element {
|
function renderCompletion(completed: SortedTimeline): JSX.Element {
|
||||||
const { idToNameMap, username } = this.props;
|
|
||||||
const { id, challengeFiles, githubLink, solution } = completed;
|
const { id, challengeFiles, githubLink, solution } = completed;
|
||||||
const completedDate = new Date(completed.completedDate);
|
const completedDate = new Date(completed.completedDate);
|
||||||
// @ts-expect-error idToNameMap is not a <string, string> Map...
|
// @ts-expect-error idToNameMap is not a <string, string> Map...
|
||||||
@ -184,9 +198,7 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
|||||||
<Link to={challengePath as string}>{challengeTitle}</Link>
|
<Link to={challengePath as string}>{challengeTitle}</Link>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>{renderViewButton(id, challengeFiles, githubLink, solution)}</td>
|
||||||
{this.renderViewButton(id, challengeFiles, githubLink, solution)}
|
|
||||||
</td>
|
|
||||||
<td className='text-center'>
|
<td className='text-center'>
|
||||||
<time dateTime={completedDate.toISOString()}>
|
<time dateTime={completedDate.toISOString()}>
|
||||||
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
||||||
@ -199,127 +211,72 @@ class TimelineInner extends Component<TimelineInnerProps, TimeLineInnerState> {
|
|||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
viewSolution(
|
|
||||||
id: string,
|
|
||||||
solution: string,
|
|
||||||
challengeFiles: ChallengeFiles
|
|
||||||
): void {
|
|
||||||
this.setState(state => ({
|
|
||||||
...state,
|
|
||||||
solutionToView: id,
|
|
||||||
solutionOpen: true,
|
|
||||||
solution,
|
|
||||||
challengeFiles
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSolution() {
|
const id = solutionToView;
|
||||||
this.setState(state => ({
|
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
|
||||||
...state,
|
const endIndex = pageNo * ITEMS_PER_PAGE;
|
||||||
solutionToView: null,
|
|
||||||
solutionOpen: false,
|
|
||||||
solution: null,
|
|
||||||
challengeFiles: null
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
firstPage() {
|
return (
|
||||||
this.setState({
|
<FullWidthRow>
|
||||||
pageNo: 1
|
<h2 className='text-center'>{t('profile.timeline')}</h2>
|
||||||
});
|
{completedMap.length === 0 ? (
|
||||||
}
|
<p className='text-center'>
|
||||||
nextPage() {
|
{t('profile.none-completed')}
|
||||||
this.setState(state => ({
|
<Link to='/learn'>{t('profile.get-started')}</Link>
|
||||||
pageNo: state.pageNo + 1
|
</p>
|
||||||
}));
|
) : (
|
||||||
}
|
<Table condensed={true} striped={true}>
|
||||||
|
<thead>
|
||||||
prevPage() {
|
<tr>
|
||||||
this.setState(state => ({
|
<th>{t('profile.challenge')}</th>
|
||||||
pageNo: state.pageNo - 1
|
<th>{t('settings.labels.solution')}</th>
|
||||||
}));
|
<th className='text-center'>{t('profile.completed')}</th>
|
||||||
}
|
</tr>
|
||||||
lastPage() {
|
</thead>
|
||||||
this.setState((_, props) => ({
|
<tbody>
|
||||||
pageNo: props.totalPages
|
{sortedTimeline.slice(startIndex, endIndex).map(renderCompletion)}
|
||||||
}));
|
</tbody>
|
||||||
}
|
</Table>
|
||||||
render() {
|
)}
|
||||||
const {
|
{id && (
|
||||||
completedMap,
|
<Modal
|
||||||
idToNameMap,
|
aria-labelledby='contained-modal-title'
|
||||||
username,
|
onHide={closeSolution}
|
||||||
sortedTimeline,
|
show={solutionOpen}
|
||||||
t,
|
>
|
||||||
totalPages = 1
|
<Modal.Header closeButton={true}>
|
||||||
} = this.props;
|
<Modal.Title id='contained-modal-title'>
|
||||||
const { solutionToView: id, solutionOpen, pageNo = 1 } = this.state;
|
{`${username}'s Solution to ${
|
||||||
const startIndex = (pageNo - 1) * ITEMS_PER_PAGE;
|
// @ts-expect-error Need better TypeDef for this
|
||||||
const endIndex = pageNo * ITEMS_PER_PAGE;
|
idToNameMap.get(id).challengeTitle as string
|
||||||
|
}`}
|
||||||
return (
|
</Modal.Title>
|
||||||
<FullWidthRow>
|
</Modal.Header>
|
||||||
<h2 className='text-center'>{t('profile.timeline')}</h2>
|
<Modal.Body>
|
||||||
{completedMap.length === 0 ? (
|
<SolutionViewer
|
||||||
<p className='text-center'>
|
challengeFiles={challengeFiles}
|
||||||
{t('profile.none-completed')}
|
solution={solution ?? ''}
|
||||||
<Link to='/learn'>{t('profile.get-started')}</Link>
|
/>
|
||||||
</p>
|
</Modal.Body>
|
||||||
) : (
|
<Modal.Footer>
|
||||||
<Table condensed={true} striped={true}>
|
<Button onClick={closeSolution}>{t('buttons.close')}</Button>
|
||||||
<thead>
|
</Modal.Footer>
|
||||||
<tr>
|
</Modal>
|
||||||
<th>{t('profile.challenge')}</th>
|
)}
|
||||||
<th>{t('settings.labels.solution')}</th>
|
{totalPages > 1 && (
|
||||||
<th className='text-center'>{t('profile.completed')}</th>
|
<TimelinePagination
|
||||||
</tr>
|
firstPage={firstPage}
|
||||||
</thead>
|
lastPage={lastPage}
|
||||||
<tbody>
|
nextPage={nextPage}
|
||||||
{sortedTimeline
|
pageNo={pageNo}
|
||||||
.slice(startIndex, endIndex)
|
prevPage={prevPage}
|
||||||
.map(this.renderCompletion)}
|
totalPages={totalPages}
|
||||||
</tbody>
|
/>
|
||||||
</Table>
|
)}
|
||||||
)}
|
</FullWidthRow>
|
||||||
{id && (
|
);
|
||||||
<Modal
|
|
||||||
aria-labelledby='contained-modal-title'
|
|
||||||
onHide={this.closeSolution}
|
|
||||||
show={solutionOpen}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='contained-modal-title'>
|
|
||||||
{`${username}'s Solution to ${
|
|
||||||
// @ts-expect-error Need better TypeDef for this
|
|
||||||
idToNameMap.get(id).challengeTitle as string
|
|
||||||
}`}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<SolutionViewer
|
|
||||||
challengeFiles={this.state.challengeFiles}
|
|
||||||
solution={this.state.solution ?? ''}
|
|
||||||
/>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={this.closeSolution}>{t('buttons.close')}</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<TimelinePagination
|
|
||||||
firstPage={this.firstPage}
|
|
||||||
lastPage={this.lastPage}
|
|
||||||
nextPage={this.nextPage}
|
|
||||||
pageNo={pageNo}
|
|
||||||
prevPage={this.prevPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FullWidthRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
|
||||||
function useIdToNameMap(): Map<string, string> {
|
function useIdToNameMap(): Map<string, string> {
|
||||||
const {
|
const {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||||
import React, { Component } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TFunction, withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -18,11 +18,6 @@ type DangerZoneProps = {
|
|||||||
t: TFunction;
|
t: TFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DangerZoneState = {
|
|
||||||
reset: boolean;
|
|
||||||
delete: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
@ -33,79 +28,65 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
|||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
|
function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
|
||||||
static displayName: string;
|
const [reset, setReset] = useState(false);
|
||||||
constructor(props: DangerZoneProps) {
|
const [delete_, setDelete] = useState(false);
|
||||||
super(props);
|
// delete is reserved
|
||||||
this.state = {
|
|
||||||
reset: false,
|
function toggleResetModal(): void {
|
||||||
delete: false
|
setReset(prev => !prev);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleResetModal = () => {
|
function toggleDeleteModal(): void {
|
||||||
return this.setState(state => ({
|
setDelete(prev => !prev);
|
||||||
...state,
|
}
|
||||||
reset: !state.reset
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleDeleteModal = () => {
|
return (
|
||||||
return this.setState(state => ({
|
<div className='danger-zone text-center'>
|
||||||
...state,
|
<FullWidthRow>
|
||||||
delete: !state.delete
|
<Panel bsStyle='danger'>
|
||||||
}));
|
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
|
||||||
};
|
<Spacer />
|
||||||
|
<p>{t('settings.danger.be-careful')}</p>
|
||||||
render() {
|
<FullWidthRow>
|
||||||
const { deleteAccount, resetProgress, t } = this.props;
|
<Button
|
||||||
return (
|
block={true}
|
||||||
<div className='danger-zone text-center'>
|
bsSize='lg'
|
||||||
<FullWidthRow>
|
bsStyle='danger'
|
||||||
<Panel bsStyle='danger'>
|
className='btn-danger'
|
||||||
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
|
onClick={toggleResetModal}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('settings.danger.reset')}
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
className='btn-danger'
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('settings.danger.delete')}
|
||||||
|
</Button>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<p>{t('settings.danger.be-careful')}</p>
|
</FullWidthRow>
|
||||||
<FullWidthRow>
|
</Panel>
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='danger'
|
|
||||||
className='btn-danger'
|
|
||||||
onClick={() => this.toggleResetModal()}
|
|
||||||
type='button'
|
|
||||||
>
|
|
||||||
{t('settings.danger.reset')}
|
|
||||||
</Button>
|
|
||||||
<ButtonSpacer />
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='danger'
|
|
||||||
className='btn-danger'
|
|
||||||
onClick={() => this.toggleDeleteModal()}
|
|
||||||
type='button'
|
|
||||||
>
|
|
||||||
{t('settings.danger.delete')}
|
|
||||||
</Button>
|
|
||||||
<Spacer />
|
|
||||||
</FullWidthRow>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<ResetModal
|
<ResetModal
|
||||||
onHide={() => this.toggleResetModal()}
|
onHide={toggleResetModal}
|
||||||
reset={resetProgress}
|
reset={resetProgress}
|
||||||
show={this.state.reset}
|
show={reset}
|
||||||
/>
|
/>
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
delete={deleteAccount}
|
delete={deleteAccount}
|
||||||
onHide={() => this.toggleDeleteModal()}
|
onHide={toggleDeleteModal}
|
||||||
show={this.state.delete}
|
show={delete_}
|
||||||
/>
|
/>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DangerZone.displayName = 'DangerZone';
|
DangerZone.displayName = 'DangerZone';
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
Button
|
Button
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { Link } from 'gatsby';
|
import { Link } from 'gatsby';
|
||||||
import React, { Component } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TFunction, Trans, withTranslation } from 'react-i18next';
|
import { TFunction, Trans, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -36,70 +36,49 @@ type EmailProps = {
|
|||||||
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
|
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EmailState = {
|
interface EmailForm {
|
||||||
emailForm: {
|
currentEmail: string;
|
||||||
currentEmail: string;
|
newEmail: string;
|
||||||
newEmail: string;
|
confirmNewEmail: string;
|
||||||
confirmNewEmail: string;
|
isPristine: boolean;
|
||||||
isPristine: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function UpdateEmailButton(this: EmailSettings): JSX.Element {
|
|
||||||
const { t } = this.props;
|
|
||||||
return (
|
|
||||||
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
|
||||||
<Button block={true} bsSize='lg' bsStyle='primary'>
|
|
||||||
{t('buttons.edit')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailSettings extends Component<EmailProps, EmailState> {
|
function EmailSettings({
|
||||||
static displayName: string;
|
email,
|
||||||
constructor(props: EmailProps) {
|
isEmailVerified,
|
||||||
super(props);
|
sendQuincyEmail,
|
||||||
|
t,
|
||||||
|
updateMyEmail,
|
||||||
|
updateQuincyEmail
|
||||||
|
}: EmailProps): JSX.Element {
|
||||||
|
const [emailForm, setEmailForm] = useState<EmailForm>({
|
||||||
|
currentEmail: email,
|
||||||
|
newEmail: '',
|
||||||
|
confirmNewEmail: '',
|
||||||
|
isPristine: true
|
||||||
|
});
|
||||||
|
|
||||||
this.state = {
|
function handleSubmit(e: React.FormEvent): void {
|
||||||
emailForm: {
|
e.preventDefault();
|
||||||
currentEmail: props.email,
|
updateMyEmail(emailForm.newEmail);
|
||||||
newEmail: '',
|
}
|
||||||
confirmNewEmail: '',
|
|
||||||
isPristine: true
|
function createHandleEmailFormChange(
|
||||||
}
|
key: 'newEmail' | 'confirmNewEmail'
|
||||||
|
): (e: React.ChangeEvent<HTMLInputElement>) => void {
|
||||||
|
return e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const userInput = e.target.value.slice();
|
||||||
|
setEmailForm(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: userInput,
|
||||||
|
isPristine: userInput === prev.currentEmail
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = (e: React.FormEvent) => {
|
function getValidationForNewEmail() {
|
||||||
e.preventDefault();
|
const { newEmail, currentEmail } = emailForm;
|
||||||
const {
|
|
||||||
emailForm: { newEmail }
|
|
||||||
} = this.state;
|
|
||||||
const { updateMyEmail } = this.props;
|
|
||||||
return updateMyEmail(newEmail);
|
|
||||||
};
|
|
||||||
|
|
||||||
createHandleEmailFormChange =
|
|
||||||
(key: 'newEmail' | 'confirmNewEmail') =>
|
|
||||||
(e: React.FormEvent<HTMLInputElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const userInput = (e.target as HTMLInputElement).value.slice();
|
|
||||||
return this.setState(state => ({
|
|
||||||
emailForm: {
|
|
||||||
...state.emailForm,
|
|
||||||
[key]: userInput,
|
|
||||||
isPristine: userInput === state.emailForm.currentEmail
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
getValidationForNewEmail = () => {
|
|
||||||
const {
|
|
||||||
emailForm: { newEmail, currentEmail }
|
|
||||||
} = this.state;
|
|
||||||
const { t } = this.props;
|
|
||||||
|
|
||||||
if (!maybeEmailRE.test(newEmail)) {
|
if (!maybeEmailRE.test(newEmail)) {
|
||||||
return {
|
return {
|
||||||
state: null,
|
state: null,
|
||||||
@ -120,14 +99,10 @@ class EmailSettings extends Component<EmailProps, EmailState> {
|
|||||||
message: t('validation.invalid-email')
|
message: t('validation.invalid-email')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
getValidationForConfirmEmail = () => {
|
|
||||||
const {
|
|
||||||
emailForm: { confirmNewEmail, newEmail }
|
|
||||||
} = this.state;
|
|
||||||
const { t } = this.props;
|
|
||||||
|
|
||||||
|
function getValidationForConfirmEmail() {
|
||||||
|
const { confirmNewEmail, newEmail } = emailForm;
|
||||||
if (!maybeEmailRE.test(newEmail)) {
|
if (!maybeEmailRE.test(newEmail)) {
|
||||||
return {
|
return {
|
||||||
state: null,
|
state: null,
|
||||||
@ -146,115 +121,109 @@ class EmailSettings extends Component<EmailProps, EmailState> {
|
|||||||
message: ''
|
message: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
const { newEmail, confirmNewEmail, currentEmail, isPristine } = emailForm;
|
||||||
const {
|
|
||||||
emailForm: { newEmail, confirmNewEmail, currentEmail, isPristine }
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const { isEmailVerified, updateQuincyEmail, sendQuincyEmail, t } =
|
const { state: newEmailValidation, message: newEmailValidationMessage } =
|
||||||
this.props;
|
getValidationForNewEmail();
|
||||||
|
|
||||||
const { state: newEmailValidation, message: newEmailValidationMessage } =
|
const {
|
||||||
this.getValidationForNewEmail();
|
state: confirmEmailValidation,
|
||||||
|
message: confirmEmailValidationMessage
|
||||||
|
} = getValidationForConfirmEmail();
|
||||||
|
|
||||||
const {
|
if (!currentEmail) {
|
||||||
state: confirmEmailValidation,
|
|
||||||
message: confirmEmailValidationMessage
|
|
||||||
} = this.getValidationForConfirmEmail();
|
|
||||||
|
|
||||||
if (!currentEmail) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FullWidthRow>
|
|
||||||
<p className='large-p text-center'>{t('settings.email.missing')}</p>
|
|
||||||
</FullWidthRow>
|
|
||||||
<FullWidthRow>
|
|
||||||
<UpdateEmailButton />
|
|
||||||
</FullWidthRow>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className='email-settings'>
|
<div>
|
||||||
<SectionHeader>{t('settings.email.heading')}</SectionHeader>
|
|
||||||
{isEmailVerified ? null : (
|
|
||||||
<FullWidthRow>
|
|
||||||
<HelpBlock>
|
|
||||||
<Alert
|
|
||||||
bsStyle='info'
|
|
||||||
className='text-center'
|
|
||||||
closeLabel={t('buttons.close')}
|
|
||||||
>
|
|
||||||
{t('settings.email.not-verified')}
|
|
||||||
<br />
|
|
||||||
<Trans i18nKey='settings.email.check'>
|
|
||||||
<Link to='/update-email' />
|
|
||||||
</Trans>
|
|
||||||
</Alert>
|
|
||||||
</HelpBlock>
|
|
||||||
</FullWidthRow>
|
|
||||||
)}
|
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<form id='form-update-email' onSubmit={this.handleSubmit}>
|
<p className='large-p text-center'>{t('settings.email.missing')}</p>
|
||||||
<FormGroup controlId='current-email'>
|
|
||||||
<ControlLabel>{t('settings.email.current')}</ControlLabel>
|
|
||||||
<FormControl.Static>{currentEmail}</FormControl.Static>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
controlId='new-email'
|
|
||||||
validationState={newEmailValidation}
|
|
||||||
>
|
|
||||||
<ControlLabel>{t('settings.email.new')}</ControlLabel>
|
|
||||||
<FormControl
|
|
||||||
onChange={this.createHandleEmailFormChange('newEmail')}
|
|
||||||
type='email'
|
|
||||||
value={newEmail}
|
|
||||||
/>
|
|
||||||
{newEmailValidationMessage ? (
|
|
||||||
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
|
|
||||||
) : null}
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
|
||||||
controlId='confirm-email'
|
|
||||||
validationState={confirmEmailValidation}
|
|
||||||
>
|
|
||||||
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
|
|
||||||
<FormControl
|
|
||||||
onChange={this.createHandleEmailFormChange('confirmNewEmail')}
|
|
||||||
type='email'
|
|
||||||
value={confirmNewEmail}
|
|
||||||
/>
|
|
||||||
{confirmEmailValidationMessage ? (
|
|
||||||
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
|
|
||||||
) : null}
|
|
||||||
</FormGroup>
|
|
||||||
<BlockSaveButton
|
|
||||||
disabled={
|
|
||||||
newEmailValidation !== 'success' ||
|
|
||||||
confirmEmailValidation !== 'success' ||
|
|
||||||
isPristine
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
<Spacer />
|
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<form id='form-quincy-email' onSubmit={this.handleSubmit}>
|
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
||||||
<ToggleSetting
|
<Button block={true} bsSize='lg' bsStyle='primary'>
|
||||||
action={t('settings.email.weekly')}
|
{t('buttons.edit')}
|
||||||
flag={sendQuincyEmail}
|
</Button>
|
||||||
flagName='sendQuincyEmail'
|
</Link>
|
||||||
offLabel={t('buttons.no-thanks')}
|
|
||||||
onLabel={t('buttons.yes-please')}
|
|
||||||
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<div className='email-settings'>
|
||||||
|
<SectionHeader>{t('settings.email.heading')}</SectionHeader>
|
||||||
|
{isEmailVerified ? null : (
|
||||||
|
<FullWidthRow>
|
||||||
|
<HelpBlock>
|
||||||
|
<Alert
|
||||||
|
bsStyle='info'
|
||||||
|
className='text-center'
|
||||||
|
closeLabel={t('buttons.close')}
|
||||||
|
>
|
||||||
|
{t('settings.email.not-verified')}
|
||||||
|
<br />
|
||||||
|
<Trans i18nKey='settings.email.check'>
|
||||||
|
<Link to='/update-email' />
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
</HelpBlock>
|
||||||
|
</FullWidthRow>
|
||||||
|
)}
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='form-update-email' onSubmit={handleSubmit}>
|
||||||
|
<FormGroup controlId='current-email'>
|
||||||
|
<ControlLabel>{t('settings.email.current')}</ControlLabel>
|
||||||
|
<FormControl.Static>{currentEmail}</FormControl.Static>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup controlId='new-email' validationState={newEmailValidation}>
|
||||||
|
<ControlLabel>{t('settings.email.new')}</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={createHandleEmailFormChange('newEmail')}
|
||||||
|
type='email'
|
||||||
|
value={newEmail}
|
||||||
|
/>
|
||||||
|
{newEmailValidationMessage ? (
|
||||||
|
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
|
||||||
|
) : null}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
controlId='confirm-email'
|
||||||
|
validationState={confirmEmailValidation}
|
||||||
|
>
|
||||||
|
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={createHandleEmailFormChange('confirmNewEmail')}
|
||||||
|
type='email'
|
||||||
|
value={confirmNewEmail}
|
||||||
|
/>
|
||||||
|
{confirmEmailValidationMessage ? (
|
||||||
|
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
|
||||||
|
) : null}
|
||||||
|
</FormGroup>
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={
|
||||||
|
newEmailValidation !== 'success' ||
|
||||||
|
confirmEmailValidation !== 'success' ||
|
||||||
|
isPristine
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='form-quincy-email' onSubmit={handleSubmit}>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.email.weekly')}
|
||||||
|
flag={sendQuincyEmail}
|
||||||
|
flagName='sendQuincyEmail'
|
||||||
|
offLabel={t('buttons.no-thanks')}
|
||||||
|
onLabel={t('buttons.yes-please')}
|
||||||
|
toggleFlag={() => updateQuincyEmail(!sendQuincyEmail)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EmailSettings.displayName = 'EmailSettings';
|
EmailSettings.displayName = 'EmailSettings';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Form } from '@freecodecamp/react-bootstrap';
|
import { Button, Form } from '@freecodecamp/react-bootstrap';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { TFunction, withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -44,142 +44,145 @@ type PrivacyProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
class PrivacySettings extends Component<PrivacyProps> {
|
function PrivacySettings({
|
||||||
static displayName: string;
|
submitProfileUI,
|
||||||
|
t,
|
||||||
handleSubmit = (e: React.FormEvent) => e.preventDefault();
|
user
|
||||||
|
}: PrivacyProps): JSX.Element {
|
||||||
toggleFlag = (flag: string) => () => {
|
function handleSubmit(e: React.FormEvent): void {
|
||||||
const privacyValues = { ...this.props.user.profileUI };
|
e.preventDefault();
|
||||||
privacyValues[flag as keyof ProfileUIType] =
|
|
||||||
!privacyValues[flag as keyof ProfileUIType];
|
|
||||||
this.props.submitProfileUI(privacyValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { t, user } = this.props;
|
|
||||||
const {
|
|
||||||
isLocked = true,
|
|
||||||
showAbout = false,
|
|
||||||
showCerts = false,
|
|
||||||
showDonation = false,
|
|
||||||
showHeatMap = false,
|
|
||||||
showLocation = false,
|
|
||||||
showName = false,
|
|
||||||
showPoints = false,
|
|
||||||
showPortfolio = false,
|
|
||||||
showTimeLine = false
|
|
||||||
} = user.profileUI;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='privacy-settings' id='privacy-settings'>
|
|
||||||
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
|
|
||||||
<FullWidthRow>
|
|
||||||
<p>{t('settings.privacy')}</p>
|
|
||||||
<Form inline={true} onSubmit={this.handleSubmit}>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-profile')}
|
|
||||||
explain={t('settings.disabled')}
|
|
||||||
flag={isLocked}
|
|
||||||
flagName='isLocked'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('isLocked')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-name')}
|
|
||||||
explain={t('settings.private-name')}
|
|
||||||
flag={!showName}
|
|
||||||
flagName='name'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showName')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-location')}
|
|
||||||
flag={!showLocation}
|
|
||||||
flagName='showLocation'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showLocation')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-about')}
|
|
||||||
flag={!showAbout}
|
|
||||||
flagName='showAbout'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showAbout')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-points')}
|
|
||||||
flag={!showPoints}
|
|
||||||
flagName='showPoints'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showPoints')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-heatmap')}
|
|
||||||
flag={!showHeatMap}
|
|
||||||
flagName='showHeatMap'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showHeatMap')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-certs')}
|
|
||||||
explain={t('settings.disabled')}
|
|
||||||
flag={!showCerts}
|
|
||||||
flagName='showCerts'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showCerts')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-portfolio')}
|
|
||||||
flag={!showPortfolio}
|
|
||||||
flagName='showPortfolio'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showPortfolio')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-timeline')}
|
|
||||||
flag={!showTimeLine}
|
|
||||||
flagName='showTimeLine'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showTimeLine')}
|
|
||||||
/>
|
|
||||||
<ToggleSetting
|
|
||||||
action={t('settings.labels.my-donations')}
|
|
||||||
flag={!showDonation}
|
|
||||||
flagName='showPortfolio'
|
|
||||||
offLabel={t('buttons.public')}
|
|
||||||
onLabel={t('buttons.private')}
|
|
||||||
toggleFlag={this.toggleFlag('showDonation')}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</FullWidthRow>
|
|
||||||
<FullWidthRow>
|
|
||||||
<Spacer />
|
|
||||||
<p>{t('settings.data')}</p>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='lg'
|
|
||||||
bsStyle='primary'
|
|
||||||
download={`${user.username}.json`}
|
|
||||||
href={`data:text/json;charset=utf-8,${encodeURIComponent(
|
|
||||||
JSON.stringify(user)
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{t('buttons.download-data')}
|
|
||||||
</Button>
|
|
||||||
</FullWidthRow>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFlag(flag: string): () => void {
|
||||||
|
return () => {
|
||||||
|
const privacyValues = { ...user.profileUI };
|
||||||
|
privacyValues[flag as keyof ProfileUIType] =
|
||||||
|
!privacyValues[flag as keyof ProfileUIType];
|
||||||
|
submitProfileUI(privacyValues);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLocked = true,
|
||||||
|
showAbout = false,
|
||||||
|
showCerts = false,
|
||||||
|
showDonation = false,
|
||||||
|
showHeatMap = false,
|
||||||
|
showLocation = false,
|
||||||
|
showName = false,
|
||||||
|
showPoints = false,
|
||||||
|
showPortfolio = false,
|
||||||
|
showTimeLine = false
|
||||||
|
} = user.profileUI;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='privacy-settings' id='privacy-settings'>
|
||||||
|
<SectionHeader>{t('settings.headings.privacy')}</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<p>{t('settings.privacy')}</p>
|
||||||
|
<Form inline={true} onSubmit={handleSubmit}>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-profile')}
|
||||||
|
explain={t('settings.disabled')}
|
||||||
|
flag={isLocked}
|
||||||
|
flagName='isLocked'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('isLocked')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-name')}
|
||||||
|
explain={t('settings.private-name')}
|
||||||
|
flag={!showName}
|
||||||
|
flagName='name'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showName')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-location')}
|
||||||
|
flag={!showLocation}
|
||||||
|
flagName='showLocation'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showLocation')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-about')}
|
||||||
|
flag={!showAbout}
|
||||||
|
flagName='showAbout'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showAbout')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-points')}
|
||||||
|
flag={!showPoints}
|
||||||
|
flagName='showPoints'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showPoints')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-heatmap')}
|
||||||
|
flag={!showHeatMap}
|
||||||
|
flagName='showHeatMap'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showHeatMap')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-certs')}
|
||||||
|
explain={t('settings.disabled')}
|
||||||
|
flag={!showCerts}
|
||||||
|
flagName='showCerts'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showCerts')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-portfolio')}
|
||||||
|
flag={!showPortfolio}
|
||||||
|
flagName='showPortfolio'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showPortfolio')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-timeline')}
|
||||||
|
flag={!showTimeLine}
|
||||||
|
flagName='showTimeLine'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showTimeLine')}
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action={t('settings.labels.my-donations')}
|
||||||
|
flag={!showDonation}
|
||||||
|
flagName='showPortfolio'
|
||||||
|
offLabel={t('buttons.public')}
|
||||||
|
onLabel={t('buttons.private')}
|
||||||
|
toggleFlag={toggleFlag('showDonation')}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FullWidthRow>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<p>{t('settings.data')}</p>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
download={`${user.username}.json`}
|
||||||
|
href={`data:text/json;charset=utf-8,${encodeURIComponent(
|
||||||
|
JSON.stringify(user)
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{t('buttons.download-data')}
|
||||||
|
</Button>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PrivacySettings.displayName = 'PrivacySettings';
|
PrivacySettings.displayName = 'PrivacySettings';
|
||||||
|
@ -9,30 +9,35 @@ interface HTMLProps {
|
|||||||
preBodyComponents?: React.ReactNode[];
|
preBodyComponents?: React.ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class HTML extends React.Component<HTMLProps> {
|
export default function HTML({
|
||||||
render(): JSX.Element {
|
body,
|
||||||
return (
|
bodyAttributes,
|
||||||
<html id='__fcc-html' {...this.props.htmlAttributes} lang='en'>
|
headComponents,
|
||||||
<head>
|
htmlAttributes,
|
||||||
<meta charSet='utf-8' />
|
postBodyComponents,
|
||||||
<meta content='ie=edge' httpEquiv='x-ua-compatible' />
|
preBodyComponents
|
||||||
<meta
|
}: HTMLProps): JSX.Element {
|
||||||
content='width=device-width, initial-scale=1.0, shrink-to-fit=no'
|
return (
|
||||||
name='viewport'
|
<html id='__fcc-html' {...htmlAttributes} lang='en'>
|
||||||
/>
|
<head>
|
||||||
{this.props.headComponents}
|
<meta charSet='utf-8' />
|
||||||
</head>
|
<meta content='ie=edge' httpEquiv='x-ua-compatible' />
|
||||||
<body {...this.props.bodyAttributes}>
|
<meta
|
||||||
{this.props.preBodyComponents}
|
content='width=device-width, initial-scale=1.0, shrink-to-fit=no'
|
||||||
<div
|
name='viewport'
|
||||||
className='tex2jax_ignore'
|
/>
|
||||||
dangerouslySetInnerHTML={{ __html: this.props.body }}
|
{headComponents}
|
||||||
id='___gatsby'
|
</head>
|
||||||
key={'body'}
|
<body {...bodyAttributes}>
|
||||||
/>
|
{preBodyComponents}
|
||||||
{this.props.postBodyComponents}
|
<div
|
||||||
</body>
|
className='tex2jax_ignore'
|
||||||
</html>
|
dangerouslySetInnerHTML={{ __html: body }}
|
||||||
);
|
id='___gatsby'
|
||||||
}
|
key={'body'}
|
||||||
|
/>
|
||||||
|
{postBodyComponents}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
import './output.css';
|
import './output.css';
|
||||||
@ -9,22 +9,19 @@ interface OutputProps {
|
|||||||
output: string[];
|
output: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Output extends Component<OutputProps> {
|
function Output({ defaultOutput, output }: OutputProps): JSX.Element {
|
||||||
render(): JSX.Element {
|
const message = sanitizeHtml(
|
||||||
const { output, defaultOutput } = this.props;
|
!isEmpty(output) ? output.join('\n') : defaultOutput,
|
||||||
const message = sanitizeHtml(
|
{
|
||||||
!isEmpty(output) ? output.join('\n') : defaultOutput,
|
allowedTags: ['b', 'i', 'em', 'strong', 'code', 'wbr']
|
||||||
{
|
}
|
||||||
allowedTags: ['b', 'i', 'em', 'strong', 'code', 'wbr']
|
);
|
||||||
}
|
return (
|
||||||
);
|
<pre
|
||||||
return (
|
className='output-text'
|
||||||
<pre
|
dangerouslySetInnerHTML={{ __html: message }}
|
||||||
className='output-text'
|
/>
|
||||||
dangerouslySetInnerHTML={{ __html: message }}
|
);
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Output;
|
export default Output;
|
||||||
|
@ -1,36 +1,28 @@
|
|||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import React, { Component } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
interface PrismFormattedProps {
|
interface PrismFormattedProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PrismFormatted extends Component<PrismFormattedProps> {
|
function PrismFormatted({ className, text }: PrismFormattedProps): JSX.Element {
|
||||||
static displayName: string;
|
const instructionsRef = useRef<HTMLDivElement>(null);
|
||||||
instructionsRef: React.RefObject<HTMLInputElement>;
|
|
||||||
componentDidMount(): void {
|
useEffect(() => {
|
||||||
// Just in case 'current' has not been created, though it should have been.
|
// Just in case 'current' has not been created, though it should have been.
|
||||||
if (this.instructionsRef.current) {
|
if (instructionsRef.current) {
|
||||||
Prism.highlightAllUnder(this.instructionsRef.current);
|
Prism.highlightAllUnder(instructionsRef.current);
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
constructor(props: PrismFormattedProps | Readonly<PrismFormattedProps>) {
|
return (
|
||||||
super(props);
|
<div
|
||||||
this.instructionsRef = React.createRef();
|
className={className}
|
||||||
}
|
dangerouslySetInnerHTML={{ __html: text }}
|
||||||
|
ref={instructionsRef}
|
||||||
render(): JSX.Element {
|
/>
|
||||||
const { text, className } = this.props;
|
);
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
|
||||||
ref={this.instructionsRef}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PrismFormatted.displayName = 'PrismFormatted';
|
PrismFormatted.displayName = 'PrismFormatted';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button } from '@freecodecamp/react-bootstrap';
|
import { Button } from '@freecodecamp/react-bootstrap';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { TFunction, withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
@ -24,34 +24,34 @@ interface ToolPanelProps {
|
|||||||
t: TFunction;
|
t: TFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ToolPanel extends Component<ToolPanelProps> {
|
export function ToolPanel({
|
||||||
static displayName: string;
|
guideUrl,
|
||||||
render(): JSX.Element {
|
openHelpModal,
|
||||||
const { guideUrl, openHelpModal, t } = this.props;
|
t
|
||||||
return (
|
}: ToolPanelProps): JSX.Element {
|
||||||
<div className='tool-panel-group project-tool-panel'>
|
return (
|
||||||
{guideUrl && (
|
<div className='tool-panel-group project-tool-panel'>
|
||||||
<Button
|
{guideUrl && (
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
href={guideUrl}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.get-hint')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-invert'
|
className='btn-invert'
|
||||||
onClick={openHelpModal}
|
href={guideUrl}
|
||||||
|
target='_blank'
|
||||||
>
|
>
|
||||||
{t('buttons.ask-for-help')}
|
{t('buttons.get-hint')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
);
|
<Button
|
||||||
}
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-invert'
|
||||||
|
onClick={openHelpModal}
|
||||||
|
>
|
||||||
|
{t('buttons.ask-for-help')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolPanel.displayName = 'ProjectToolPanel';
|
ToolPanel.displayName = 'ProjectToolPanel';
|
||||||
|
Reference in New Issue
Block a user