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