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:
Niraj Nandish
2021-06-25 20:01:11 +04:00
committed by Mrugesh Mohapatra
parent 5ad374cc1a
commit de888d640c
16 changed files with 367 additions and 262 deletions

View File

@ -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>;

View File

@ -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';

View File

@ -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);

View File

@ -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,

View File

@ -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;

View File

@ -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,17 +82,19 @@ class EmailSettings extends Component {
return updateMyEmail(newEmail); return updateMyEmail(newEmail);
}; };
createHandleEmailFormChange = key => e => { createHandleEmailFormChange =
e.preventDefault(); (key: 'newEmail' | 'confirmNewEmail') =>
const userInput = e.target.value.slice(); (e: React.FormEvent<HTMLInputElement>) => {
return this.setState(state => ({ e.preventDefault();
emailForm: { const userInput = (e.target as HTMLInputElement).value.slice();
...state.emailForm, return this.setState(state => ({
[key]: userInput, emailForm: {
isPristine: userInput === state.emailForm.currentEmail ...state.emailForm,
} [key]: userInput,
})); isPristine: userInput === state.emailForm.currentEmail
}; }
}));
};
getValidationForNewEmail = () => { getValidationForNewEmail = () => {
const { const {
@ -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,

View File

@ -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;

View File

@ -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,45 +96,50 @@ class InternetSettings extends Component {
}; };
} }
createHandleChange = key => e => { createHandleChange =
const value = e.target.value.slice(0); (key: keyof InternetFormValues) =>
return this.setState(state => ({ (e: React.FormEvent<HTMLInputElement>) => {
formValues: { const value = (e.target as HTMLInputElement).value.slice(0);
...state.formValues, return this.setState(state => ({
[key]: value formValues: {
} ...state.formValues,
})); [key]: value
}; }
}));
};
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(
const maybeUrl = formValues[key]; (bool: boolean, key: keyof InternetFormValues): boolean => {
return maybeUrl ? isURL(maybeUrl) : bool; const maybeUrl = formValues[key];
}, false); return maybeUrl ? isURL(maybeUrl) : bool;
},
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);

View File

@ -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,24 +70,26 @@ class PortfolioSettings extends Component {
}; };
} }
createOnChangeHandler = (id, key) => e => { createOnChangeHandler =
e.preventDefault(); (id: string, key: 'description' | 'image' | 'title' | 'url') =>
const userInput = e.target.value.slice(); (e: React.FormEvent<HTMLInputElement>) => {
return this.setState(state => { e.preventDefault();
const { portfolio: currentPortfolio } = state; const userInput = (e.target as HTMLInputElement).value.slice();
const mutablePortfolio = currentPortfolio.slice(0); return this.setState(state => {
const index = findIndex(currentPortfolio, p => p.id === id); const { portfolio: currentPortfolio } = state;
const mutablePortfolio = currentPortfolio.slice(0);
const index = findIndex(currentPortfolio, p => p.id === id);
mutablePortfolio[index] = { mutablePortfolio[index] = {
...mutablePortfolio[index], ...mutablePortfolio[index],
[key]: userInput [key]: userInput
}; };
return { portfolio: mutablePortfolio }; return { portfolio: mutablePortfolio };
}); });
}; };
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);

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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';