feat(client): ts-migrate Settings component (#42341)
* feat(client): change file format to Typescript * feat(client): migrate SectionHeader component * feat(client): migrate ToggleSetting component * feat(client): migrate Theme component * feat(client): migrate ResetModal component * feat(client): migrate DeleteModal component * feat(client): migrate DangerZone component * feat(client): migrate Honesty component * feat(client): migrate Privacy component * feat(client): migrate Username component * feat(client): migrate About component * feat(client): migrate Email component * feat(client): migrate Internet component * feat(client): migrate Portfolio component * feat(client): add required types packages * feat(client): undo file format change * feat(client): rename files to kebab-case * feat(client): rename temp files to kebab-case * feat(client): Review ts-migration * Fix imports * Fix some types * Remove unncessary comments * Consistent comment format * restore mistakenly deleted file
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
5ad374cc1a
commit
de888d640c
@ -16,15 +16,15 @@ import { createFlashMessage } from '../components/Flash/redux';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Loader, Spacer } from '../components/helpers';
|
import { Loader, Spacer } from '../components/helpers';
|
||||||
import About from '../components/settings/About';
|
import About from '../components/settings/about';
|
||||||
import Privacy from '../components/settings/Privacy';
|
import Privacy from '../components/settings/privacy';
|
||||||
import Email from '../components/settings/Email';
|
import Email from '../components/settings/email';
|
||||||
import Internet from '../components/settings/Internet';
|
import Internet from '../components/settings/internet';
|
||||||
import Portfolio from '../components/settings/Portfolio';
|
import Portfolio from '../components/settings/portfolio';
|
||||||
import Honesty from '../components/settings/Honesty';
|
import Honesty from '../components/settings/honesty';
|
||||||
import Certification from '../components/settings/Certification';
|
import Certification from '../components/settings/Certification';
|
||||||
import DangerZone from '../components/settings/DangerZone';
|
|
||||||
import { UserType } from '../redux/prop-types';
|
import { UserType } from '../redux/prop-types';
|
||||||
|
import DangerZone from '../components/settings/danger-zone';
|
||||||
|
|
||||||
const { apiLocation } = envData as Record<string, string>;
|
const { apiLocation } = envData as Record<string, string>;
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
legacyProjectMap
|
legacyProjectMap
|
||||||
} from '../../resources/certAndProjectMap';
|
} from '../../resources/certAndProjectMap';
|
||||||
|
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
import ProjectModal from '../SolutionViewer/ProjectModal';
|
import ProjectModal from '../SolutionViewer/ProjectModal';
|
||||||
import { FullWidthRow, Spacer } from '../helpers';
|
import { FullWidthRow, Spacer } from '../helpers';
|
||||||
|
|
||||||
|
@ -1,34 +1,51 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
HelpBlock,
|
HelpBlock,
|
||||||
Alert
|
Alert
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import { FullWidthRow, Spacer } from '../helpers';
|
import { FullWidthRow, Spacer } from '../helpers';
|
||||||
import ThemeSettings from './Theme';
|
import ThemeSettings from './theme';
|
||||||
import UsernameSettings from './Username';
|
import UsernameSettings from './username';
|
||||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const propTypes = {
|
type FormValues = {
|
||||||
about: PropTypes.string,
|
name: string;
|
||||||
currentTheme: PropTypes.string,
|
location: string;
|
||||||
location: PropTypes.string,
|
picture: string;
|
||||||
name: PropTypes.string,
|
about: string;
|
||||||
picture: PropTypes.string,
|
|
||||||
points: PropTypes.number,
|
|
||||||
submitNewAbout: PropTypes.func.isRequired,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
toggleNightMode: PropTypes.func.isRequired,
|
|
||||||
username: PropTypes.string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class AboutSettings extends Component {
|
type AboutProps = {
|
||||||
constructor(props) {
|
about: string;
|
||||||
|
currentTheme: string;
|
||||||
|
location: string;
|
||||||
|
name: string;
|
||||||
|
picture: string;
|
||||||
|
points: number;
|
||||||
|
submitNewAbout: (formValues: FormValues) => void;
|
||||||
|
t: (str: string) => string;
|
||||||
|
toggleNightMode: () => void;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AboutState = {
|
||||||
|
formValues: FormValues;
|
||||||
|
originalValues: FormValues;
|
||||||
|
formClicked: boolean;
|
||||||
|
isPictureUrlValid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AboutSettings extends Component<AboutProps, AboutState> {
|
||||||
|
validationImage: HTMLImageElement;
|
||||||
|
static displayName: string;
|
||||||
|
constructor(props: AboutProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.validationImage = new Image();
|
this.validationImage = new Image();
|
||||||
const { name = '', location = '', picture = '', about = '' } = props;
|
const { name = '', location = '', picture = '', about = '' } = props;
|
||||||
@ -56,7 +73,7 @@ class AboutSettings extends Component {
|
|||||||
picture === formValues.picture &&
|
picture === formValues.picture &&
|
||||||
about === formValues.about
|
about === formValues.about
|
||||||
) {
|
) {
|
||||||
/* eslint-disable-next-line react/no-did-update-set-state */
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
return this.setState({
|
return this.setState({
|
||||||
originalValues: {
|
originalValues: {
|
||||||
name,
|
name,
|
||||||
@ -74,13 +91,13 @@ class AboutSettings extends Component {
|
|||||||
const { formValues, originalValues } = this.state;
|
const { formValues, originalValues } = this.state;
|
||||||
return (
|
return (
|
||||||
this.state.isPictureUrlValid === false ||
|
this.state.isPictureUrlValid === false ||
|
||||||
Object.keys(originalValues)
|
(Object.keys(originalValues) as Array<keyof FormValues>)
|
||||||
.map(key => originalValues[key] === formValues[key])
|
.map(key => originalValues[key] === formValues[key])
|
||||||
.every(bool => bool)
|
.every(bool => bool)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { formValues } = this.state;
|
const { formValues } = this.state;
|
||||||
const { submitNewAbout } = this.props;
|
const { submitNewAbout } = this.props;
|
||||||
@ -93,8 +110,8 @@ class AboutSettings extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleNameChange = e => {
|
handleNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value.slice(0);
|
const value = (e.target as HTMLInputElement).value.slice(0);
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
formValues: {
|
formValues: {
|
||||||
...state.formValues,
|
...state.formValues,
|
||||||
@ -103,8 +120,8 @@ class AboutSettings extends Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleLocationChange = e => {
|
handleLocationChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value.slice(0);
|
const value = (e.target as HTMLInputElement).value.slice(0);
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
formValues: {
|
formValues: {
|
||||||
...state.formValues,
|
...state.formValues,
|
||||||
@ -126,8 +143,8 @@ class AboutSettings extends Component {
|
|||||||
loadEvent = () => this.setState({ isPictureUrlValid: true });
|
loadEvent = () => this.setState({ isPictureUrlValid: true });
|
||||||
errorEvent = () => this.setState({ isPictureUrlValid: false });
|
errorEvent = () => this.setState({ isPictureUrlValid: false });
|
||||||
|
|
||||||
handlePictureChange = e => {
|
handlePictureChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value.slice(0);
|
const value = (e.target as HTMLInputElement).value.slice(0);
|
||||||
this.validationImage.src = value;
|
this.validationImage.src = value;
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
formValues: {
|
formValues: {
|
||||||
@ -150,8 +167,8 @@ class AboutSettings extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAboutChange = e => {
|
handleAboutChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const value = e.target.value.slice(0);
|
const value = (e.target as HTMLInputElement).value.slice(0);
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
formValues: {
|
formValues: {
|
||||||
...state.formValues,
|
...state.formValues,
|
||||||
@ -229,6 +246,5 @@ class AboutSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AboutSettings.displayName = 'AboutSettings';
|
AboutSettings.displayName = 'AboutSettings';
|
||||||
AboutSettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withTranslation()(AboutSettings);
|
export default withTranslation()(AboutSettings);
|
@ -1,25 +1,32 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
|
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
|
||||||
import { deleteAccount, resetProgress } from '../../redux/settings';
|
import { deleteAccount, resetProgress } from '../../redux/settings';
|
||||||
import DeleteModal from './DeleteModal';
|
import DeleteModal from './delete-modal';
|
||||||
import ResetModal from './ResetModal';
|
import ResetModal from './reset-modal';
|
||||||
|
|
||||||
import './danger-zone.css';
|
import './danger-zone.css';
|
||||||
|
|
||||||
const propTypes = {
|
type DangerZoneProps = {
|
||||||
deleteAccount: PropTypes.func.isRequired,
|
deleteAccount: () => void;
|
||||||
resetProgress: PropTypes.func.isRequired,
|
resetProgress: () => void;
|
||||||
t: PropTypes.func.isRequired
|
t: (str: string) => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DangerZoneState = {
|
||||||
|
reset: boolean;
|
||||||
|
delete: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
@ -28,8 +35,9 @@ const mapDispatchToProps = dispatch =>
|
|||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
class DangerZone extends Component {
|
class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
|
||||||
constructor(props) {
|
static displayName: string;
|
||||||
|
constructor(props: DangerZoneProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
reset: false,
|
reset: false,
|
||||||
@ -103,7 +111,6 @@ class DangerZone extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DangerZone.displayName = 'DangerZone';
|
DangerZone.displayName = 'DangerZone';
|
||||||
DangerZone.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
@ -1,19 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { ButtonSpacer } from '../helpers';
|
import { ButtonSpacer } from '../helpers';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import './danger-zone.css';
|
import './danger-zone.css';
|
||||||
|
|
||||||
const propTypes = {
|
type DeleteModalProps = {
|
||||||
delete: PropTypes.func.isRequired,
|
delete: () => void;
|
||||||
onHide: PropTypes.func.isRequired,
|
onHide: () => void;
|
||||||
show: PropTypes.bool
|
show: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DeleteModal(props) {
|
function DeleteModal(props: DeleteModalProps): JSX.Element {
|
||||||
const { show, onHide } = props;
|
const { show, onHide } = props;
|
||||||
const email = 'team@freecodecamp.org';
|
const email = 'team@freecodecamp.org';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -36,7 +37,7 @@ function DeleteModal(props) {
|
|||||||
<p>{t('settings.danger.delete-p1')}</p>
|
<p>{t('settings.danger.delete-p1')}</p>
|
||||||
<p>{t('settings.danger.delete-p2')}</p>
|
<p>{t('settings.danger.delete-p2')}</p>
|
||||||
<p>
|
<p>
|
||||||
<Trans email={email} i18nKey='settings.danger.delete-p3'>
|
<Trans i18nKey='settings.danger.delete-p3'>
|
||||||
<a href={`mailto:${email}`} title={email}>
|
<a href={`mailto:${email}`} title={email}>
|
||||||
{{ email }}
|
{{ email }}
|
||||||
</a>
|
</a>
|
||||||
@ -73,6 +74,5 @@ function DeleteModal(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DeleteModal.displayName = 'DeleteModal';
|
DeleteModal.displayName = 'DeleteModal';
|
||||||
DeleteModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default DeleteModal;
|
export default DeleteModal;
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'gatsby';
|
import { Link } from 'gatsby';
|
||||||
@ -10,33 +9,45 @@ import {
|
|||||||
ControlLabel,
|
ControlLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
Button
|
Button
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import isEmail from 'validator/lib/isEmail';
|
import isEmail from 'validator/lib/isEmail';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { updateMyEmail } from '../../redux/settings';
|
import { updateMyEmail } from '../../redux/settings';
|
||||||
import { maybeEmailRE } from '../../utils';
|
import { maybeEmailRE } from '../../utils';
|
||||||
|
|
||||||
import FullWidthRow from '../helpers/full-width-row';
|
import FullWidthRow from '../helpers/full-width-row';
|
||||||
import Spacer from '../helpers/spacer';
|
import Spacer from '../helpers/spacer';
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||||
import ToggleSetting from './ToggleSetting';
|
import ToggleSetting from './toggle-setting';
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators({ updateMyEmail }, dispatch);
|
bindActionCreators({ updateMyEmail }, dispatch);
|
||||||
|
|
||||||
const propTypes = {
|
type EmailProps = {
|
||||||
email: PropTypes.string,
|
email: string;
|
||||||
isEmailVerified: PropTypes.bool,
|
isEmailVerified: boolean;
|
||||||
sendQuincyEmail: PropTypes.bool,
|
sendQuincyEmail: boolean;
|
||||||
t: PropTypes.func.isRequired,
|
t: (str: string) => string;
|
||||||
updateMyEmail: PropTypes.func.isRequired,
|
updateMyEmail: (email: string) => void;
|
||||||
updateQuincyEmail: PropTypes.func.isRequired
|
updateQuincyEmail: (sendQuincyEmail: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UpdateEmailButton() {
|
type EmailState = {
|
||||||
|
emailForm: {
|
||||||
|
currentEmail: string;
|
||||||
|
newEmail: string;
|
||||||
|
confirmNewEmail: string;
|
||||||
|
isPristine: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UpdateEmailButton(this: EmailSettings): JSX.Element {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
return (
|
return (
|
||||||
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
<Link style={{ textDecoration: 'none' }} to='/update-email'>
|
||||||
@ -47,8 +58,9 @@ export function UpdateEmailButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailSettings extends Component {
|
class EmailSettings extends Component<EmailProps, EmailState> {
|
||||||
constructor(props) {
|
static displayName: string;
|
||||||
|
constructor(props: EmailProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -61,7 +73,7 @@ class EmailSettings extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const {
|
const {
|
||||||
emailForm: { newEmail }
|
emailForm: { newEmail }
|
||||||
@ -70,9 +82,11 @@ class EmailSettings extends Component {
|
|||||||
return updateMyEmail(newEmail);
|
return updateMyEmail(newEmail);
|
||||||
};
|
};
|
||||||
|
|
||||||
createHandleEmailFormChange = key => e => {
|
createHandleEmailFormChange =
|
||||||
|
(key: 'newEmail' | 'confirmNewEmail') =>
|
||||||
|
(e: React.FormEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const userInput = e.target.value.slice();
|
const userInput = (e.target as HTMLInputElement).value.slice();
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
emailForm: {
|
emailForm: {
|
||||||
...state.emailForm,
|
...state.emailForm,
|
||||||
@ -242,7 +256,6 @@ class EmailSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EmailSettings.displayName = 'EmailSettings';
|
EmailSettings.displayName = 'EmailSettings';
|
||||||
EmailSettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
@ -1,20 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FullWidthRow } from '../helpers';
|
import { FullWidthRow } from '../helpers';
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
import HonestyPolicy from '../../resources/honesty-policy';
|
import HonestyPolicy from '../../resources/honesty-policy';
|
||||||
|
|
||||||
import './honesty.css';
|
import './honesty.css';
|
||||||
|
|
||||||
const propTypes = {
|
type HonestyProps = {
|
||||||
isHonest: PropTypes.bool,
|
isHonest: boolean;
|
||||||
updateIsHonest: PropTypes.func.isRequired
|
updateIsHonest: (obj: { isHonest: boolean }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Honesty = ({ isHonest, updateIsHonest }) => {
|
const Honesty = ({ isHonest, updateIsHonest }: HonestyProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const button = isHonest ? (
|
const button = isHonest ? (
|
||||||
<Button
|
<Button
|
||||||
@ -49,6 +50,5 @@ const Honesty = ({ isHonest, updateIsHonest }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Honesty.displayName = 'Honesty';
|
Honesty.displayName = 'Honesty';
|
||||||
Honesty.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default Honesty;
|
export default Honesty;
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Fragment, Component } from 'react';
|
import React, { Fragment, Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
import {
|
import {
|
||||||
@ -7,27 +6,38 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
ControlLabel
|
ControlLabel
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import isURL from 'validator/lib/isURL';
|
import isURL from 'validator/lib/isURL';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { maybeUrlRE } from '../../utils';
|
import { maybeUrlRE } from '../../utils';
|
||||||
|
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
import { FullWidthRow } from '../helpers';
|
import { FullWidthRow } from '../helpers';
|
||||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||||
|
|
||||||
const propTypes = {
|
interface InternetFormValues {
|
||||||
githubProfile: PropTypes.string,
|
githubProfile: string;
|
||||||
linkedin: PropTypes.string,
|
linkedin: string;
|
||||||
t: PropTypes.func.isRequired,
|
twitter: string;
|
||||||
twitter: PropTypes.string,
|
website: string;
|
||||||
updateInternetSettings: PropTypes.func.isRequired,
|
}
|
||||||
website: PropTypes.string
|
|
||||||
|
interface InternetProps extends InternetFormValues {
|
||||||
|
t: (str: string) => string;
|
||||||
|
updateInternetSettings: (formValues: InternetFormValues) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternetState = {
|
||||||
|
formValues: InternetFormValues;
|
||||||
|
originalValues: InternetFormValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
class InternetSettings extends Component {
|
class InternetSettings extends Component<InternetProps, InternetState> {
|
||||||
constructor(props) {
|
static displayName: string;
|
||||||
|
constructor(props: InternetProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const {
|
const {
|
||||||
githubProfile = '',
|
githubProfile = '',
|
||||||
@ -58,7 +68,7 @@ class InternetSettings extends Component {
|
|||||||
twitter !== originalValues.twitter ||
|
twitter !== originalValues.twitter ||
|
||||||
website !== originalValues.website
|
website !== originalValues.website
|
||||||
) {
|
) {
|
||||||
/* eslint-disable-next-line react/no-did-update-set-state */
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
return this.setState({
|
return this.setState({
|
||||||
originalValues: { githubProfile, linkedin, twitter, website }
|
originalValues: { githubProfile, linkedin, twitter, website }
|
||||||
});
|
});
|
||||||
@ -86,8 +96,10 @@ class InternetSettings extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createHandleChange = key => e => {
|
createHandleChange =
|
||||||
const value = e.target.value.slice(0);
|
(key: keyof InternetFormValues) =>
|
||||||
|
(e: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
const value = (e.target as HTMLInputElement).value.slice(0);
|
||||||
return this.setState(state => ({
|
return this.setState(state => ({
|
||||||
formValues: {
|
formValues: {
|
||||||
...state.formValues,
|
...state.formValues,
|
||||||
@ -98,33 +110,36 @@ class InternetSettings extends Component {
|
|||||||
|
|
||||||
isFormPristine = () => {
|
isFormPristine = () => {
|
||||||
const { formValues, originalValues } = this.state;
|
const { formValues, originalValues } = this.state;
|
||||||
return Object.keys(originalValues)
|
return (Object.keys(originalValues) as Array<keyof InternetFormValues>)
|
||||||
.map(key => originalValues[key] === formValues[key])
|
.map(key => originalValues[key] === formValues[key])
|
||||||
.every(bool => bool);
|
.every(bool => bool);
|
||||||
};
|
};
|
||||||
|
|
||||||
isFormValid = () => {
|
isFormValid = (): boolean => {
|
||||||
const { formValues, originalValues } = this.state;
|
const { formValues, originalValues } = this.state;
|
||||||
const valueReducer = obj => {
|
const valueReducer = (obj: InternetFormValues) => {
|
||||||
return Object.values(obj).reduce(
|
return Object.values(obj).reduce(
|
||||||
(acc, cur) => (acc ? acc : cur !== ''),
|
(acc, cur): boolean => (acc ? acc : cur !== ''),
|
||||||
false
|
false
|
||||||
);
|
) as boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let formHasValues = valueReducer(formValues);
|
const formHasValues = valueReducer(formValues);
|
||||||
let OriginalHasValues = valueReducer(originalValues);
|
const OriginalHasValues = valueReducer(originalValues);
|
||||||
|
|
||||||
// check if user had values but wants to delete them all
|
// check if user had values but wants to delete them all
|
||||||
if (OriginalHasValues && !formHasValues) return true;
|
if (OriginalHasValues && !formHasValues) return true;
|
||||||
|
|
||||||
return Object.keys(formValues).reduce((bool, key) => {
|
return (Object.keys(formValues) as Array<keyof InternetFormValues>).reduce(
|
||||||
|
(bool: boolean, key: keyof InternetFormValues): boolean => {
|
||||||
const maybeUrl = formValues[key];
|
const maybeUrl = formValues[key];
|
||||||
return maybeUrl ? isURL(maybeUrl) : bool;
|
return maybeUrl ? isURL(maybeUrl) : bool;
|
||||||
}, false);
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!this.isFormPristine() && this.isFormValid()) {
|
if (!this.isFormPristine() && this.isFormValid()) {
|
||||||
// // Only submit the form if is has changed, and if it is valid
|
// // Only submit the form if is has changed, and if it is valid
|
||||||
@ -142,10 +157,10 @@ class InternetSettings extends Component {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
renderHelpBlock = validationMessage =>
|
renderHelpBlock = (validationMessage: string) =>
|
||||||
validationMessage ? <HelpBlock>{validationMessage}</HelpBlock> : null;
|
validationMessage ? <HelpBlock>{validationMessage}</HelpBlock> : null;
|
||||||
|
|
||||||
renderCheck = (url, validation) =>
|
renderCheck = (url: string, validation: string | null) =>
|
||||||
url && validation === 'success' ? (
|
url && validation === 'success' ? (
|
||||||
<FormControl.Feedback>
|
<FormControl.Feedback>
|
||||||
<span>
|
<span>
|
||||||
@ -246,6 +261,5 @@ class InternetSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InternetSettings.displayName = 'InternetSettings';
|
InternetSettings.displayName = 'InternetSettings';
|
||||||
InternetSettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withTranslation()(InternetSettings);
|
export default withTranslation()(InternetSettings);
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -7,6 +6,8 @@ import {
|
|||||||
ControlLabel,
|
ControlLabel,
|
||||||
FormControl,
|
FormControl,
|
||||||
HelpBlock
|
HelpBlock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { findIndex, find, isEqual } from 'lodash-es';
|
import { findIndex, find, isEqual } from 'lodash-es';
|
||||||
import isURL from 'validator/lib/isURL';
|
import isURL from 'validator/lib/isURL';
|
||||||
@ -15,22 +16,27 @@ import { withTranslation } from 'react-i18next';
|
|||||||
import { hasProtocolRE } from '../../utils';
|
import { hasProtocolRE } from '../../utils';
|
||||||
|
|
||||||
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
|
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||||
|
|
||||||
const propTypes = {
|
type PortfolioValues = {
|
||||||
picture: PropTypes.string,
|
id: string;
|
||||||
portfolio: PropTypes.arrayOf(
|
description: string;
|
||||||
PropTypes.shape({
|
image: string;
|
||||||
description: PropTypes.string,
|
title: string;
|
||||||
image: PropTypes.string,
|
url: string;
|
||||||
title: PropTypes.string,
|
};
|
||||||
url: PropTypes.string
|
|
||||||
})
|
type PortfolioProps = {
|
||||||
),
|
picture?: string;
|
||||||
t: PropTypes.func.isRequired,
|
portfolio: PortfolioValues[];
|
||||||
updatePortfolio: PropTypes.func.isRequired,
|
t: (str: string, obj?: { charsLeft: number }) => string;
|
||||||
username: PropTypes.string
|
updatePortfolio: (obj: { portfolio: PortfolioValues[] }) => void;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PortfolioState = {
|
||||||
|
portfolio: PortfolioValues[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function createEmptyPortfolio() {
|
function createEmptyPortfolio() {
|
||||||
@ -43,16 +49,18 @@ function createEmptyPortfolio() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFindById(id) {
|
function createFindById(id: string) {
|
||||||
return p => p.id === id;
|
return (p: PortfolioValues) => p.id === id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockEvent = {
|
const mockEvent = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
preventDefault() {}
|
preventDefault() {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class PortfolioSettings extends Component {
|
class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
|
||||||
constructor(props) {
|
static displayName: string;
|
||||||
|
constructor(props: PortfolioProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { portfolio = [] } = props;
|
const { portfolio = [] } = props;
|
||||||
@ -62,9 +70,11 @@ class PortfolioSettings extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createOnChangeHandler = (id, key) => e => {
|
createOnChangeHandler =
|
||||||
|
(id: string, key: 'description' | 'image' | 'title' | 'url') =>
|
||||||
|
(e: React.FormEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const userInput = e.target.value.slice();
|
const userInput = (e.target as HTMLInputElement).value.slice();
|
||||||
return this.setState(state => {
|
return this.setState(state => {
|
||||||
const { portfolio: currentPortfolio } = state;
|
const { portfolio: currentPortfolio } = state;
|
||||||
const mutablePortfolio = currentPortfolio.slice(0);
|
const mutablePortfolio = currentPortfolio.slice(0);
|
||||||
@ -79,7 +89,7 @@ class PortfolioSettings extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSubmit = e => {
|
handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { updatePortfolio } = this.props;
|
const { updatePortfolio } = this.props;
|
||||||
const { portfolio } = this.state;
|
const { portfolio } = this.state;
|
||||||
@ -92,7 +102,7 @@ class PortfolioSettings extends Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRemoveItem = id => {
|
handleRemoveItem = (id: string) => {
|
||||||
return this.setState(
|
return this.setState(
|
||||||
state => ({
|
state => ({
|
||||||
portfolio: state.portfolio.filter(p => p.id !== id)
|
portfolio: state.portfolio.filter(p => p.id !== id)
|
||||||
@ -101,7 +111,7 @@ class PortfolioSettings extends Component {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
isFormPristine = id => {
|
isFormPristine = (id: string) => {
|
||||||
const { portfolio } = this.state;
|
const { portfolio } = this.state;
|
||||||
const { portfolio: originalPortfolio } = this.props;
|
const { portfolio: originalPortfolio } = this.props;
|
||||||
const original = find(originalPortfolio, createFindById(id));
|
const original = find(originalPortfolio, createFindById(id));
|
||||||
@ -112,25 +122,26 @@ class PortfolioSettings extends Component {
|
|||||||
return isEqual(original, edited);
|
return isEqual(original, edited);
|
||||||
};
|
};
|
||||||
|
|
||||||
isFormValid = id => {
|
// TODO: Check if this function is required or not
|
||||||
const { portfolio } = this.state;
|
// isFormValid = id => {
|
||||||
const toValidate = find(portfolio, createFindById(id));
|
// const { portfolio } = this.state;
|
||||||
if (!toValidate) {
|
// const toValidate = find(portfolio, createFindById(id));
|
||||||
return false;
|
// if (!toValidate) {
|
||||||
}
|
// return false;
|
||||||
const { title, url, image, description } = toValidate;
|
// }
|
||||||
|
// const { title, url, image, description } = toValidate;
|
||||||
|
|
||||||
const { state: titleState } = this.getTitleValidation(title);
|
// const { state: titleState } = this.getTitleValidation(title);
|
||||||
const { state: urlState } = this.getUrlValidation(url);
|
// const { state: urlState } = this.getUrlValidation(url);
|
||||||
const { state: imageState } = this.getUrlValidation(image, true);
|
// const { state: imageState } = this.getUrlValidation(image, true);
|
||||||
const { state: descriptionState } =
|
// const { state: descriptionState } =
|
||||||
this.getDescriptionValidation(description);
|
// this.getDescriptionValidation(description);
|
||||||
return [titleState, imageState, urlState, descriptionState]
|
// return [titleState, imageState, urlState, descriptionState]
|
||||||
.filter(Boolean)
|
// .filter(Boolean)
|
||||||
.every(state => state === 'success');
|
// .every(state => state === 'success');
|
||||||
};
|
// };
|
||||||
|
|
||||||
getDescriptionValidation(description) {
|
getDescriptionValidation(description: string) {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const len = description.length;
|
const len = description.length;
|
||||||
const charsLeft = 288 - len;
|
const charsLeft = 288 - len;
|
||||||
@ -152,7 +163,7 @@ class PortfolioSettings extends Component {
|
|||||||
return { state: 'success', message: '' };
|
return { state: 'success', message: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitleValidation(title) {
|
getTitleValidation(title: string) {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return { state: 'error', message: t('validation.title-required') };
|
return { state: 'error', message: t('validation.title-required') };
|
||||||
@ -167,7 +178,7 @@ class PortfolioSettings extends Component {
|
|||||||
return { state: 'success', message: '' };
|
return { state: 'success', message: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrlValidation(maybeUrl, isImage) {
|
getUrlValidation(maybeUrl: string, isImage?: boolean) {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const len = maybeUrl.length;
|
const len = maybeUrl.length;
|
||||||
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
|
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
|
||||||
@ -187,7 +198,11 @@ class PortfolioSettings extends Component {
|
|||||||
: { state: 'warning', message: t('validation.use-valid-url') };
|
: { state: 'warning', message: t('validation.use-valid-url') };
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPortfolio = (portfolio, index, arr) => {
|
renderPortfolio = (
|
||||||
|
portfolio: PortfolioValues,
|
||||||
|
index: number,
|
||||||
|
arr: PortfolioValues[]
|
||||||
|
) => {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const { id, title, description, url, image } = portfolio;
|
const { id, title, description, url, image } = portfolio;
|
||||||
const pristine = this.isFormPristine(id);
|
const pristine = this.isFormPristine(id);
|
||||||
@ -330,6 +345,5 @@ class PortfolioSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PortfolioSettings.displayName = 'PortfolioSettings';
|
PortfolioSettings.displayName = 'PortfolioSettings';
|
||||||
PortfolioSettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default withTranslation()(PortfolioSettings);
|
export default withTranslation()(PortfolioSettings);
|
@ -1,52 +1,60 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Button, Form } from '@freecodecamp/react-bootstrap';
|
import { Button, Form } from '@freecodecamp/react-bootstrap';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import { userSelector } from '../../redux';
|
import { userSelector } from '../../redux';
|
||||||
import { submitProfileUI } from '../../redux/settings';
|
import { submitProfileUI } from '../../redux/settings';
|
||||||
|
|
||||||
import FullWidthRow from '../helpers/full-width-row';
|
import FullWidthRow from '../helpers/full-width-row';
|
||||||
import Spacer from '../helpers/spacer';
|
import Spacer from '../helpers/spacer';
|
||||||
import ToggleSetting from './ToggleSetting';
|
import ToggleSetting from './toggle-setting';
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './section-header';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(userSelector, user => ({
|
const mapStateToProps = createSelector(userSelector, user => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
user
|
user
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators({ submitProfileUI }, dispatch);
|
bindActionCreators({ submitProfileUI }, dispatch);
|
||||||
|
|
||||||
const propTypes = {
|
type ProfileUIType = {
|
||||||
submitProfileUI: PropTypes.func.isRequired,
|
isLocked: boolean;
|
||||||
t: PropTypes.func.isRequired,
|
showAbout: boolean;
|
||||||
user: PropTypes.shape({
|
showCerts: boolean;
|
||||||
profileUI: PropTypes.shape({
|
showDonation: boolean;
|
||||||
isLocked: PropTypes.bool,
|
showHeatMap: boolean;
|
||||||
showAbout: PropTypes.bool,
|
showLocation: boolean;
|
||||||
showCerts: PropTypes.bool,
|
showName: boolean;
|
||||||
showDonation: PropTypes.bool,
|
showPoints: boolean;
|
||||||
showHeatMap: PropTypes.bool,
|
showPortfolio: boolean;
|
||||||
showLocation: PropTypes.bool,
|
showTimeLine: boolean;
|
||||||
showName: PropTypes.bool,
|
|
||||||
showPoints: PropTypes.bool,
|
|
||||||
showPortfolio: PropTypes.bool,
|
|
||||||
showTimeLine: PropTypes.bool
|
|
||||||
}),
|
|
||||||
username: PropTypes.String
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PrivacySettings extends Component {
|
type PrivacyProps = {
|
||||||
handleSubmit = e => e.preventDefault();
|
submitProfileUI: (profileUI: ProfileUIType) => void;
|
||||||
|
t: (str: string) => string;
|
||||||
|
user: {
|
||||||
|
profileUI: ProfileUIType;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
toggleFlag = flag => () => {
|
class PrivacySettings extends Component<PrivacyProps> {
|
||||||
|
static displayName: string;
|
||||||
|
|
||||||
|
handleSubmit = (e: React.FormEvent) => e.preventDefault();
|
||||||
|
|
||||||
|
toggleFlag = (flag: string) => () => {
|
||||||
const privacyValues = { ...this.props.user.profileUI };
|
const privacyValues = { ...this.props.user.profileUI };
|
||||||
privacyValues[flag] = !privacyValues[flag];
|
privacyValues[flag as keyof ProfileUIType] =
|
||||||
|
!privacyValues[flag as keyof ProfileUIType];
|
||||||
this.props.submitProfileUI(privacyValues);
|
this.props.submitProfileUI(privacyValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,7 +184,6 @@ class PrivacySettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PrivacySettings.displayName = 'PrivacySettings';
|
PrivacySettings.displayName = 'PrivacySettings';
|
||||||
PrivacySettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
@ -1,17 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { ButtonSpacer } from '../helpers';
|
import { ButtonSpacer } from '../helpers';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
const propTypes = {
|
type ResetModalProps = {
|
||||||
onHide: PropTypes.func.isRequired,
|
onHide: () => void;
|
||||||
reset: PropTypes.func.isRequired,
|
reset: () => void;
|
||||||
show: PropTypes.bool
|
show: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ResetModal(props) {
|
function ResetModal(props: ResetModalProps): JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { show, onHide } = props;
|
const { show, onHide } = props;
|
||||||
|
|
||||||
@ -64,6 +65,5 @@ function ResetModal(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ResetModal.displayName = 'ResetModal';
|
ResetModal.displayName = 'ResetModal';
|
||||||
ResetModal.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default ResetModal;
|
export default ResetModal;
|
@ -1,17 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import FullWidthRow from '../helpers/full-width-row';
|
import FullWidthRow from '../helpers/full-width-row';
|
||||||
|
|
||||||
const propTypes = {
|
type SectionHeaderProps = {
|
||||||
children: PropTypes.oneOfType([
|
children: string | React.ReactNode | React.ReactElement;
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.element,
|
|
||||||
PropTypes.node
|
|
||||||
])
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function SectionHeader({ children }) {
|
function SectionHeader({ children }: SectionHeaderProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<h2 className='text-center'>{children}</h2>
|
<h2 className='text-center'>{children}</h2>
|
||||||
@ -21,6 +16,5 @@ function SectionHeader({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SectionHeader.displayName = 'SectionHeader';
|
SectionHeader.displayName = 'SectionHeader';
|
||||||
SectionHeader.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default SectionHeader;
|
export default SectionHeader;
|
@ -1,20 +1,27 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
import { Form } from '@freecodecamp/react-bootstrap';
|
import { Form } from '@freecodecamp/react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ToggleSetting from './ToggleSetting';
|
import ToggleSetting from './toggle-setting';
|
||||||
|
|
||||||
const propTypes = {
|
type ThemeProps = {
|
||||||
currentTheme: PropTypes.string.isRequired,
|
currentTheme: string;
|
||||||
toggleNightMode: PropTypes.func.isRequired
|
toggleNightMode: (theme: 'default' | 'night') => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ThemeSettings({ currentTheme, toggleNightMode }) {
|
export default function ThemeSettings({
|
||||||
|
currentTheme,
|
||||||
|
toggleNightMode
|
||||||
|
}: ThemeProps): JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form inline={true} onSubmit={e => e.preventDefault()}>
|
<Form
|
||||||
|
inline={true}
|
||||||
|
onSubmit={(e: React.FormEvent): void => e.preventDefault()}
|
||||||
|
>
|
||||||
<ToggleSetting
|
<ToggleSetting
|
||||||
action={t('settings.labels.night-mode')}
|
action={t('settings.labels.night-mode')}
|
||||||
flag={currentTheme === 'night'}
|
flag={currentTheme === 'night'}
|
||||||
@ -30,4 +37,3 @@ export default function ThemeSettings({ currentTheme, toggleNightMode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeSettings.displayName = 'ThemeSettings';
|
ThemeSettings.displayName = 'ThemeSettings';
|
||||||
ThemeSettings.propTypes = propTypes;
|
|
@ -1,9 +1,10 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
ControlLabel,
|
ControlLabel,
|
||||||
HelpBlock
|
HelpBlock
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import TB from '../helpers/toggle-button';
|
import TB from '../helpers/toggle-button';
|
||||||
@ -11,12 +12,14 @@ import { ButtonSpacer } from '../helpers';
|
|||||||
|
|
||||||
import './toggle-setting.css';
|
import './toggle-setting.css';
|
||||||
|
|
||||||
const propTypes = {
|
type ToggleSettingProps = {
|
||||||
action: PropTypes.string.isRequired,
|
action: string;
|
||||||
explain: PropTypes.string,
|
explain?: string;
|
||||||
flag: PropTypes.bool.isRequired,
|
flag: boolean;
|
||||||
flagName: PropTypes.string.isRequired,
|
flagName: string;
|
||||||
toggleFlag: PropTypes.func.isRequired
|
toggleFlag: () => void;
|
||||||
|
offLabel: string;
|
||||||
|
onLabel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ToggleSetting({
|
export default function ToggleSetting({
|
||||||
@ -26,7 +29,7 @@ export default function ToggleSetting({
|
|||||||
flagName,
|
flagName,
|
||||||
toggleFlag,
|
toggleFlag,
|
||||||
...restProps
|
...restProps
|
||||||
}) {
|
}: ToggleSettingProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className='toggle-setting-container'>
|
<div className='toggle-setting-container'>
|
||||||
@ -53,4 +56,3 @@ export default function ToggleSetting({
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToggleSetting.displayName = 'ToggleSetting';
|
ToggleSetting.displayName = 'ToggleSetting';
|
||||||
ToggleSetting.propTypes = propTypes;
|
|
@ -1,5 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
@ -8,8 +8,11 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
Alert,
|
Alert,
|
||||||
FormGroup
|
FormGroup
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateUsername,
|
validateUsername,
|
||||||
@ -20,24 +23,46 @@ import FullWidthRow from '../helpers/full-width-row';
|
|||||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||||
import { isValidUsername } from '../../../../utils/validate';
|
import { isValidUsername } from '../../../../utils/validate';
|
||||||
|
|
||||||
const propTypes = {
|
type UsernameProps = {
|
||||||
isValidUsername: PropTypes.bool,
|
isValidUsername: boolean;
|
||||||
submitNewUsername: PropTypes.func.isRequired,
|
submitNewUsername: (name: string) => void;
|
||||||
t: PropTypes.func.isRequired,
|
t: (str: string, obj?: { username: string }) => string;
|
||||||
username: PropTypes.string,
|
username: string;
|
||||||
validateUsername: PropTypes.func.isRequired,
|
validateUsername: (name: string) => void;
|
||||||
validating: PropTypes.bool
|
validating: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UsernameState = {
|
||||||
|
isFormPristine: boolean;
|
||||||
|
formValue: string;
|
||||||
|
characterValidation: {
|
||||||
|
valid: boolean;
|
||||||
|
error: null | string;
|
||||||
|
};
|
||||||
|
submitClicked: boolean;
|
||||||
|
isUserNew: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
usernameValidationSelector,
|
usernameValidationSelector,
|
||||||
({ isValidUsername, fetchState }) => ({
|
({
|
||||||
|
isValidUsername,
|
||||||
|
fetchState
|
||||||
|
}: {
|
||||||
|
isValidUsername: boolean;
|
||||||
|
fetchState: {
|
||||||
|
pending: boolean;
|
||||||
|
complete: boolean;
|
||||||
|
errored: boolean;
|
||||||
|
error: boolean | null;
|
||||||
|
};
|
||||||
|
}) => ({
|
||||||
isValidUsername,
|
isValidUsername,
|
||||||
validating: fetchState.pending
|
validating: fetchState.pending
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
submitNewUsername,
|
submitNewUsername,
|
||||||
@ -49,8 +74,9 @@ const mapDispatchToProps = dispatch =>
|
|||||||
const hex = '[0-9a-f]';
|
const hex = '[0-9a-f]';
|
||||||
const tempUserRegex = new RegExp(`^fcc${hex}{8}-(${hex}{4}-){3}${hex}{12}$`);
|
const tempUserRegex = new RegExp(`^fcc${hex}{8}-(${hex}{4}-){3}${hex}{12}$`);
|
||||||
|
|
||||||
class UsernameSettings extends Component {
|
class UsernameSettings extends Component<UsernameProps, UsernameState> {
|
||||||
constructor(props) {
|
static displayName: string;
|
||||||
|
constructor(props: UsernameProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -66,13 +92,13 @@ class UsernameSettings extends Component {
|
|||||||
this.validateFormInput = this.validateFormInput.bind(this);
|
this.validateFormInput = this.validateFormInput.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps: UsernameProps, prevState: UsernameState) {
|
||||||
const { username: prevUsername } = prevProps;
|
const { username: prevUsername } = prevProps;
|
||||||
const { formValue: prevFormValue } = prevState;
|
const { formValue: prevFormValue } = prevState;
|
||||||
const { username } = this.props;
|
const { username } = this.props;
|
||||||
const { formValue } = this.state;
|
const { formValue } = this.state;
|
||||||
if (prevUsername !== username && prevFormValue === formValue) {
|
if (prevUsername !== username && prevFormValue === formValue) {
|
||||||
/* eslint-disable-next-line react/no-did-update-set-state */
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
return this.setState({
|
return this.setState({
|
||||||
isFormPristine: username === formValue,
|
isFormPristine: username === formValue,
|
||||||
submitClicked: false,
|
submitClicked: false,
|
||||||
@ -82,7 +108,7 @@ class UsernameSettings extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit(e) {
|
handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { submitNewUsername } = this.props;
|
const { submitNewUsername } = this.props;
|
||||||
const {
|
const {
|
||||||
@ -95,10 +121,10 @@ class UsernameSettings extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e) {
|
handleChange(e: React.FormEvent<HTMLInputElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { username, validateUsername } = this.props;
|
const { username, validateUsername } = this.props;
|
||||||
const newValue = e.target.value;
|
const newValue = (e.target as HTMLInputElement).value;
|
||||||
return this.setState(
|
return this.setState(
|
||||||
{
|
{
|
||||||
formValue: newValue,
|
formValue: newValue,
|
||||||
@ -113,11 +139,15 @@ class UsernameSettings extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateFormInput(formValue) {
|
validateFormInput(formValue: string) {
|
||||||
return isValidUsername(formValue);
|
return isValidUsername(formValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAlerts(validating, error, isValidUsername) {
|
renderAlerts(
|
||||||
|
validating: boolean,
|
||||||
|
error: string | null,
|
||||||
|
isValidUsername: boolean
|
||||||
|
) {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
|
|
||||||
if (!validating && error) {
|
if (!validating && error) {
|
||||||
@ -203,7 +233,6 @@ class UsernameSettings extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UsernameSettings.displayName = 'UsernameSettings';
|
UsernameSettings.displayName = 'UsernameSettings';
|
||||||
UsernameSettings.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
3
client/src/declarations.d.ts
vendored
3
client/src/declarations.d.ts
vendored
@ -1,3 +1,6 @@
|
|||||||
// eslint-disable-next-line import/unambiguous
|
// eslint-disable-next-line import/unambiguous
|
||||||
declare module '@freecodecamp/react-bootstrap';
|
declare module '@freecodecamp/react-bootstrap';
|
||||||
declare module '@freecodecamp/strip-comments';
|
declare module '@freecodecamp/strip-comments';
|
||||||
|
declare module '@types/react-redux';
|
||||||
|
declare module '@types/validator';
|
||||||
|
declare module '@types/lodash-es';
|
||||||
|
Reference in New Issue
Block a user