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 { Loader, Spacer } from '../components/helpers';
import About from '../components/settings/About';
import Privacy from '../components/settings/Privacy';
import Email from '../components/settings/Email';
import Internet from '../components/settings/Internet';
import Portfolio from '../components/settings/Portfolio';
import Honesty from '../components/settings/Honesty';
import About from '../components/settings/about';
import Privacy from '../components/settings/privacy';
import Email from '../components/settings/email';
import Internet from '../components/settings/internet';
import Portfolio from '../components/settings/portfolio';
import Honesty from '../components/settings/honesty';
import Certification from '../components/settings/Certification';
import DangerZone from '../components/settings/DangerZone';
import { UserType } from '../redux/prop-types';
import DangerZone from '../components/settings/danger-zone';
const { apiLocation } = envData as Record<string, string>;

View File

@ -16,7 +16,7 @@ import {
legacyProjectMap
} from '../../resources/certAndProjectMap';
import SectionHeader from './SectionHeader';
import SectionHeader from './section-header';
import ProjectModal from '../SolutionViewer/ProjectModal';
import { FullWidthRow, Spacer } from '../helpers';

View File

@ -1,34 +1,51 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
FormGroup,
ControlLabel,
FormControl,
HelpBlock,
Alert
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import { FullWidthRow, Spacer } from '../helpers';
import ThemeSettings from './Theme';
import UsernameSettings from './Username';
import ThemeSettings from './theme';
import UsernameSettings from './username';
import BlockSaveButton from '../helpers/form/block-save-button';
import { withTranslation } from 'react-i18next';
const propTypes = {
about: PropTypes.string,
currentTheme: PropTypes.string,
location: PropTypes.string,
name: PropTypes.string,
picture: PropTypes.string,
points: PropTypes.number,
submitNewAbout: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
toggleNightMode: PropTypes.func.isRequired,
username: PropTypes.string
type FormValues = {
name: string;
location: string;
picture: string;
about: string;
};
class AboutSettings extends Component {
constructor(props) {
type AboutProps = {
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);
this.validationImage = new Image();
const { name = '', location = '', picture = '', about = '' } = props;
@ -56,7 +73,7 @@ class AboutSettings extends Component {
picture === formValues.picture &&
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({
originalValues: {
name,
@ -74,13 +91,13 @@ class AboutSettings extends Component {
const { formValues, originalValues } = this.state;
return (
this.state.isPictureUrlValid === false ||
Object.keys(originalValues)
(Object.keys(originalValues) as Array<keyof FormValues>)
.map(key => originalValues[key] === formValues[key])
.every(bool => bool)
);
};
handleSubmit = e => {
handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const { formValues } = this.state;
const { submitNewAbout } = this.props;
@ -93,8 +110,8 @@ class AboutSettings extends Component {
}
};
handleNameChange = e => {
const value = e.target.value.slice(0);
handleNameChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
...state.formValues,
@ -103,8 +120,8 @@ class AboutSettings extends Component {
}));
};
handleLocationChange = e => {
const value = e.target.value.slice(0);
handleLocationChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
...state.formValues,
@ -126,8 +143,8 @@ class AboutSettings extends Component {
loadEvent = () => this.setState({ isPictureUrlValid: true });
errorEvent = () => this.setState({ isPictureUrlValid: false });
handlePictureChange = e => {
const value = e.target.value.slice(0);
handlePictureChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
this.validationImage.src = value;
return this.setState(state => ({
formValues: {
@ -150,8 +167,8 @@ class AboutSettings extends Component {
}
};
handleAboutChange = e => {
const value = e.target.value.slice(0);
handleAboutChange = (e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
...state.formValues,
@ -229,6 +246,5 @@ class AboutSettings extends Component {
}
AboutSettings.displayName = 'AboutSettings';
AboutSettings.propTypes = propTypes;
export default withTranslation()(AboutSettings);

View File

@ -1,25 +1,32 @@
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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
import { deleteAccount, resetProgress } from '../../redux/settings';
import DeleteModal from './DeleteModal';
import ResetModal from './ResetModal';
import DeleteModal from './delete-modal';
import ResetModal from './reset-modal';
import './danger-zone.css';
const propTypes = {
deleteAccount: PropTypes.func.isRequired,
resetProgress: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
type DangerZoneProps = {
deleteAccount: () => void;
resetProgress: () => void;
t: (str: string) => JSX.Element;
};
type DangerZoneState = {
reset: boolean;
delete: boolean;
};
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
deleteAccount,
@ -28,8 +35,9 @@ const mapDispatchToProps = dispatch =>
dispatch
);
class DangerZone extends Component {
constructor(props) {
class DangerZone extends Component<DangerZoneProps, DangerZoneState> {
static displayName: string;
constructor(props: DangerZoneProps) {
super(props);
this.state = {
reset: false,
@ -103,7 +111,6 @@ class DangerZone extends Component {
}
DangerZone.displayName = 'DangerZone';
DangerZone.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,19 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
import { ButtonSpacer } from '../helpers';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import './danger-zone.css';
const propTypes = {
delete: PropTypes.func.isRequired,
onHide: PropTypes.func.isRequired,
show: PropTypes.bool
type DeleteModalProps = {
delete: () => void;
onHide: () => void;
show: boolean;
};
function DeleteModal(props) {
function DeleteModal(props: DeleteModalProps): JSX.Element {
const { show, onHide } = props;
const email = 'team@freecodecamp.org';
const { t } = useTranslation();
@ -36,7 +37,7 @@ function DeleteModal(props) {
<p>{t('settings.danger.delete-p1')}</p>
<p>{t('settings.danger.delete-p2')}</p>
<p>
<Trans email={email} i18nKey='settings.danger.delete-p3'>
<Trans i18nKey='settings.danger.delete-p3'>
<a href={`mailto:${email}`} title={email}>
{{ email }}
</a>
@ -73,6 +74,5 @@ function DeleteModal(props) {
}
DeleteModal.displayName = 'DeleteModal';
DeleteModal.propTypes = propTypes;
export default DeleteModal;

View File

@ -1,5 +1,4 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Link } from 'gatsby';
@ -10,33 +9,45 @@ import {
ControlLabel,
FormControl,
Button
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import isEmail from 'validator/lib/isEmail';
import { Trans, withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import { updateMyEmail } from '../../redux/settings';
import { maybeEmailRE } from '../../utils';
import FullWidthRow from '../helpers/full-width-row';
import Spacer from '../helpers/spacer';
import SectionHeader from './SectionHeader';
import SectionHeader from './section-header';
import BlockSaveButton from '../helpers/form/block-save-button';
import ToggleSetting from './ToggleSetting';
import ToggleSetting from './toggle-setting';
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ updateMyEmail }, dispatch);
const propTypes = {
email: PropTypes.string,
isEmailVerified: PropTypes.bool,
sendQuincyEmail: PropTypes.bool,
t: PropTypes.func.isRequired,
updateMyEmail: PropTypes.func.isRequired,
updateQuincyEmail: PropTypes.func.isRequired
type EmailProps = {
email: string;
isEmailVerified: boolean;
sendQuincyEmail: boolean;
t: (str: string) => string;
updateMyEmail: (email: string) => void;
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;
return (
<Link style={{ textDecoration: 'none' }} to='/update-email'>
@ -47,8 +58,9 @@ export function UpdateEmailButton() {
);
}
class EmailSettings extends Component {
constructor(props) {
class EmailSettings extends Component<EmailProps, EmailState> {
static displayName: string;
constructor(props: EmailProps) {
super(props);
this.state = {
@ -61,7 +73,7 @@ class EmailSettings extends Component {
};
}
handleSubmit = e => {
handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const {
emailForm: { newEmail }
@ -70,17 +82,19 @@ class EmailSettings extends Component {
return updateMyEmail(newEmail);
};
createHandleEmailFormChange = key => e => {
e.preventDefault();
const userInput = e.target.value.slice();
return this.setState(state => ({
emailForm: {
...state.emailForm,
[key]: userInput,
isPristine: userInput === state.emailForm.currentEmail
}
}));
};
createHandleEmailFormChange =
(key: 'newEmail' | 'confirmNewEmail') =>
(e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault();
const userInput = (e.target as HTMLInputElement).value.slice();
return this.setState(state => ({
emailForm: {
...state.emailForm,
[key]: userInput,
isPristine: userInput === state.emailForm.currentEmail
}
}));
};
getValidationForNewEmail = () => {
const {
@ -242,7 +256,6 @@ class EmailSettings extends Component {
}
EmailSettings.displayName = 'EmailSettings';
EmailSettings.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,20 +1,21 @@
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 { useTranslation } from 'react-i18next';
import { FullWidthRow } from '../helpers';
import SectionHeader from './SectionHeader';
import SectionHeader from './section-header';
import HonestyPolicy from '../../resources/honesty-policy';
import './honesty.css';
const propTypes = {
isHonest: PropTypes.bool,
updateIsHonest: PropTypes.func.isRequired
type HonestyProps = {
isHonest: boolean;
updateIsHonest: (obj: { isHonest: boolean }) => void;
};
const Honesty = ({ isHonest, updateIsHonest }) => {
const Honesty = ({ isHonest, updateIsHonest }: HonestyProps): JSX.Element => {
const { t } = useTranslation();
const button = isHonest ? (
<Button
@ -49,6 +50,5 @@ const Honesty = ({ isHonest, updateIsHonest }) => {
};
Honesty.displayName = 'Honesty';
Honesty.propTypes = propTypes;
export default Honesty;

View File

@ -1,5 +1,4 @@
import React, { Fragment, Component } from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import {
@ -7,27 +6,38 @@ import {
FormControl,
FormGroup,
ControlLabel
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import isURL from 'validator/lib/isURL';
import { withTranslation } from 'react-i18next';
import { maybeUrlRE } from '../../utils';
import SectionHeader from './SectionHeader';
import SectionHeader from './section-header';
import { FullWidthRow } from '../helpers';
import BlockSaveButton from '../helpers/form/block-save-button';
const propTypes = {
githubProfile: PropTypes.string,
linkedin: PropTypes.string,
t: PropTypes.func.isRequired,
twitter: PropTypes.string,
updateInternetSettings: PropTypes.func.isRequired,
website: PropTypes.string
interface InternetFormValues {
githubProfile: string;
linkedin: string;
twitter: string;
website: string;
}
interface InternetProps extends InternetFormValues {
t: (str: string) => string;
updateInternetSettings: (formValues: InternetFormValues) => void;
}
type InternetState = {
formValues: InternetFormValues;
originalValues: InternetFormValues;
};
class InternetSettings extends Component {
constructor(props) {
class InternetSettings extends Component<InternetProps, InternetState> {
static displayName: string;
constructor(props: InternetProps) {
super(props);
const {
githubProfile = '',
@ -58,7 +68,7 @@ class InternetSettings extends Component {
twitter !== originalValues.twitter ||
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({
originalValues: { githubProfile, linkedin, twitter, website }
});
@ -86,45 +96,50 @@ class InternetSettings extends Component {
};
}
createHandleChange = key => e => {
const value = e.target.value.slice(0);
return this.setState(state => ({
formValues: {
...state.formValues,
[key]: value
}
}));
};
createHandleChange =
(key: keyof InternetFormValues) =>
(e: React.FormEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
...state.formValues,
[key]: value
}
}));
};
isFormPristine = () => {
const { formValues, originalValues } = this.state;
return Object.keys(originalValues)
return (Object.keys(originalValues) as Array<keyof InternetFormValues>)
.map(key => originalValues[key] === formValues[key])
.every(bool => bool);
};
isFormValid = () => {
isFormValid = (): boolean => {
const { formValues, originalValues } = this.state;
const valueReducer = obj => {
const valueReducer = (obj: InternetFormValues) => {
return Object.values(obj).reduce(
(acc, cur) => (acc ? acc : cur !== ''),
(acc, cur): boolean => (acc ? acc : cur !== ''),
false
);
) as boolean;
};
let formHasValues = valueReducer(formValues);
let OriginalHasValues = valueReducer(originalValues);
const formHasValues = valueReducer(formValues);
const OriginalHasValues = valueReducer(originalValues);
// check if user had values but wants to delete them all
if (OriginalHasValues && !formHasValues) return true;
return Object.keys(formValues).reduce((bool, key) => {
const maybeUrl = formValues[key];
return maybeUrl ? isURL(maybeUrl) : bool;
}, false);
return (Object.keys(formValues) as Array<keyof InternetFormValues>).reduce(
(bool: boolean, key: keyof InternetFormValues): boolean => {
const maybeUrl = formValues[key];
return maybeUrl ? isURL(maybeUrl) : bool;
},
false
);
};
handleSubmit = e => {
handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!this.isFormPristine() && this.isFormValid()) {
// // Only submit the form if is has changed, and if it is valid
@ -142,10 +157,10 @@ class InternetSettings extends Component {
return null;
};
renderHelpBlock = validationMessage =>
renderHelpBlock = (validationMessage: string) =>
validationMessage ? <HelpBlock>{validationMessage}</HelpBlock> : null;
renderCheck = (url, validation) =>
renderCheck = (url: string, validation: string | null) =>
url && validation === 'success' ? (
<FormControl.Feedback>
<span>
@ -246,6 +261,5 @@ class InternetSettings extends Component {
}
InternetSettings.displayName = 'InternetSettings';
InternetSettings.propTypes = propTypes;
export default withTranslation()(InternetSettings);

View File

@ -1,5 +1,4 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { nanoid } from 'nanoid';
import {
Button,
@ -7,6 +6,8 @@ import {
ControlLabel,
FormControl,
HelpBlock
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import { findIndex, find, isEqual } from 'lodash-es';
import isURL from 'validator/lib/isURL';
@ -15,22 +16,27 @@ import { withTranslation } from 'react-i18next';
import { hasProtocolRE } from '../../utils';
import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers';
import SectionHeader from './SectionHeader';
import SectionHeader from './section-header';
import BlockSaveButton from '../helpers/form/block-save-button';
const propTypes = {
picture: PropTypes.string,
portfolio: PropTypes.arrayOf(
PropTypes.shape({
description: PropTypes.string,
image: PropTypes.string,
title: PropTypes.string,
url: PropTypes.string
})
),
t: PropTypes.func.isRequired,
updatePortfolio: PropTypes.func.isRequired,
username: PropTypes.string
type PortfolioValues = {
id: string;
description: string;
image: string;
title: string;
url: string;
};
type PortfolioProps = {
picture?: string;
portfolio: PortfolioValues[];
t: (str: string, obj?: { charsLeft: number }) => string;
updatePortfolio: (obj: { portfolio: PortfolioValues[] }) => void;
username?: string;
};
type PortfolioState = {
portfolio: PortfolioValues[];
};
function createEmptyPortfolio() {
@ -43,16 +49,18 @@ function createEmptyPortfolio() {
};
}
function createFindById(id) {
return p => p.id === id;
function createFindById(id: string) {
return (p: PortfolioValues) => p.id === id;
}
const mockEvent = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
preventDefault() {}
};
class PortfolioSettings extends Component {
constructor(props) {
class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
static displayName: string;
constructor(props: PortfolioProps) {
super(props);
const { portfolio = [] } = props;
@ -62,24 +70,26 @@ class PortfolioSettings extends Component {
};
}
createOnChangeHandler = (id, key) => e => {
e.preventDefault();
const userInput = e.target.value.slice();
return this.setState(state => {
const { portfolio: currentPortfolio } = state;
const mutablePortfolio = currentPortfolio.slice(0);
const index = findIndex(currentPortfolio, p => p.id === id);
createOnChangeHandler =
(id: string, key: 'description' | 'image' | 'title' | 'url') =>
(e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault();
const userInput = (e.target as HTMLInputElement).value.slice();
return this.setState(state => {
const { portfolio: currentPortfolio } = state;
const mutablePortfolio = currentPortfolio.slice(0);
const index = findIndex(currentPortfolio, p => p.id === id);
mutablePortfolio[index] = {
...mutablePortfolio[index],
[key]: userInput
};
mutablePortfolio[index] = {
...mutablePortfolio[index],
[key]: userInput
};
return { portfolio: mutablePortfolio };
});
};
return { portfolio: mutablePortfolio };
});
};
handleSubmit = e => {
handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const { updatePortfolio } = this.props;
const { portfolio } = this.state;
@ -92,7 +102,7 @@ class PortfolioSettings extends Component {
}));
};
handleRemoveItem = id => {
handleRemoveItem = (id: string) => {
return this.setState(
state => ({
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: originalPortfolio } = this.props;
const original = find(originalPortfolio, createFindById(id));
@ -112,25 +122,26 @@ class PortfolioSettings extends Component {
return isEqual(original, edited);
};
isFormValid = id => {
const { portfolio } = this.state;
const toValidate = find(portfolio, createFindById(id));
if (!toValidate) {
return false;
}
const { title, url, image, description } = toValidate;
// TODO: Check if this function is required or not
// isFormValid = id => {
// const { portfolio } = this.state;
// const toValidate = find(portfolio, createFindById(id));
// if (!toValidate) {
// return false;
// }
// const { title, url, image, description } = toValidate;
const { state: titleState } = this.getTitleValidation(title);
const { state: urlState } = this.getUrlValidation(url);
const { state: imageState } = this.getUrlValidation(image, true);
const { state: descriptionState } =
this.getDescriptionValidation(description);
return [titleState, imageState, urlState, descriptionState]
.filter(Boolean)
.every(state => state === 'success');
};
// const { state: titleState } = this.getTitleValidation(title);
// const { state: urlState } = this.getUrlValidation(url);
// const { state: imageState } = this.getUrlValidation(image, true);
// const { state: descriptionState } =
// this.getDescriptionValidation(description);
// return [titleState, imageState, urlState, descriptionState]
// .filter(Boolean)
// .every(state => state === 'success');
// };
getDescriptionValidation(description) {
getDescriptionValidation(description: string) {
const { t } = this.props;
const len = description.length;
const charsLeft = 288 - len;
@ -152,7 +163,7 @@ class PortfolioSettings extends Component {
return { state: 'success', message: '' };
}
getTitleValidation(title) {
getTitleValidation(title: string) {
const { t } = this.props;
if (!title) {
return { state: 'error', message: t('validation.title-required') };
@ -167,7 +178,7 @@ class PortfolioSettings extends Component {
return { state: 'success', message: '' };
}
getUrlValidation(maybeUrl, isImage) {
getUrlValidation(maybeUrl: string, isImage?: boolean) {
const { t } = this.props;
const len = maybeUrl.length;
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
@ -187,7 +198,11 @@ class PortfolioSettings extends Component {
: { state: 'warning', message: t('validation.use-valid-url') };
}
renderPortfolio = (portfolio, index, arr) => {
renderPortfolio = (
portfolio: PortfolioValues,
index: number,
arr: PortfolioValues[]
) => {
const { t } = this.props;
const { id, title, description, url, image } = portfolio;
const pristine = this.isFormPristine(id);
@ -330,6 +345,5 @@ class PortfolioSettings extends Component {
}
PortfolioSettings.displayName = 'PortfolioSettings';
PortfolioSettings.propTypes = propTypes;
export default withTranslation()(PortfolioSettings);

View File

@ -1,52 +1,60 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { Button, Form } from '@freecodecamp/react-bootstrap';
import { withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import { userSelector } from '../../redux';
import { submitProfileUI } from '../../redux/settings';
import FullWidthRow from '../helpers/full-width-row';
import Spacer from '../helpers/spacer';
import ToggleSetting from './ToggleSetting';
import SectionHeader from './SectionHeader';
import ToggleSetting from './toggle-setting';
import SectionHeader from './section-header';
const mapStateToProps = createSelector(userSelector, user => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
user
}));
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ submitProfileUI }, dispatch);
const propTypes = {
submitProfileUI: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
user: PropTypes.shape({
profileUI: PropTypes.shape({
isLocked: PropTypes.bool,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showDonation: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool
}),
username: PropTypes.String
})
type ProfileUIType = {
isLocked: boolean;
showAbout: boolean;
showCerts: boolean;
showDonation: boolean;
showHeatMap: boolean;
showLocation: boolean;
showName: boolean;
showPoints: boolean;
showPortfolio: boolean;
showTimeLine: boolean;
};
class PrivacySettings extends Component {
handleSubmit = e => e.preventDefault();
type PrivacyProps = {
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 };
privacyValues[flag] = !privacyValues[flag];
privacyValues[flag as keyof ProfileUIType] =
!privacyValues[flag as keyof ProfileUIType];
this.props.submitProfileUI(privacyValues);
};
@ -176,7 +184,6 @@ class PrivacySettings extends Component {
}
PrivacySettings.displayName = 'PrivacySettings';
PrivacySettings.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,17 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { ButtonSpacer } from '../helpers';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { Button, Modal } from '@freecodecamp/react-bootstrap';
const propTypes = {
onHide: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
show: PropTypes.bool
type ResetModalProps = {
onHide: () => void;
reset: () => void;
show: boolean;
};
function ResetModal(props) {
function ResetModal(props: ResetModalProps): JSX.Element {
const { t } = useTranslation();
const { show, onHide } = props;
@ -64,6 +65,5 @@ function ResetModal(props) {
}
ResetModal.displayName = 'ResetModal';
ResetModal.propTypes = propTypes;
export default ResetModal;

View File

@ -1,17 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import FullWidthRow from '../helpers/full-width-row';
const propTypes = {
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
PropTypes.node
])
type SectionHeaderProps = {
children: string | React.ReactNode | React.ReactElement;
};
function SectionHeader({ children }) {
function SectionHeader({ children }: SectionHeaderProps): JSX.Element {
return (
<FullWidthRow>
<h2 className='text-center'>{children}</h2>
@ -21,6 +16,5 @@ function SectionHeader({ children }) {
}
SectionHeader.displayName = 'SectionHeader';
SectionHeader.propTypes = propTypes;
export default SectionHeader;

View File

@ -1,20 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { Form } from '@freecodecamp/react-bootstrap';
import { useTranslation } from 'react-i18next';
import ToggleSetting from './ToggleSetting';
import ToggleSetting from './toggle-setting';
const propTypes = {
currentTheme: PropTypes.string.isRequired,
toggleNightMode: PropTypes.func.isRequired
type ThemeProps = {
currentTheme: string;
toggleNightMode: (theme: 'default' | 'night') => void;
};
export default function ThemeSettings({ currentTheme, toggleNightMode }) {
export default function ThemeSettings({
currentTheme,
toggleNightMode
}: ThemeProps): JSX.Element {
const { t } = useTranslation();
return (
<Form inline={true} onSubmit={e => e.preventDefault()}>
<Form
inline={true}
onSubmit={(e: React.FormEvent): void => e.preventDefault()}
>
<ToggleSetting
action={t('settings.labels.night-mode')}
flag={currentTheme === 'night'}
@ -30,4 +37,3 @@ export default function ThemeSettings({ currentTheme, toggleNightMode }) {
}
ThemeSettings.displayName = 'ThemeSettings';
ThemeSettings.propTypes = propTypes;

View File

@ -1,9 +1,10 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
FormGroup,
ControlLabel,
HelpBlock
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import TB from '../helpers/toggle-button';
@ -11,12 +12,14 @@ import { ButtonSpacer } from '../helpers';
import './toggle-setting.css';
const propTypes = {
action: PropTypes.string.isRequired,
explain: PropTypes.string,
flag: PropTypes.bool.isRequired,
flagName: PropTypes.string.isRequired,
toggleFlag: PropTypes.func.isRequired
type ToggleSettingProps = {
action: string;
explain?: string;
flag: boolean;
flagName: string;
toggleFlag: () => void;
offLabel: string;
onLabel: string;
};
export default function ToggleSetting({
@ -26,7 +29,7 @@ export default function ToggleSetting({
flagName,
toggleFlag,
...restProps
}) {
}: ToggleSettingProps): JSX.Element {
return (
<Fragment>
<div className='toggle-setting-container'>
@ -53,4 +56,3 @@ export default function 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 PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
@ -8,8 +8,11 @@ import {
FormControl,
Alert,
FormGroup
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from '@freecodecamp/react-bootstrap';
import { withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
import {
validateUsername,
@ -20,24 +23,46 @@ import FullWidthRow from '../helpers/full-width-row';
import BlockSaveButton from '../helpers/form/block-save-button';
import { isValidUsername } from '../../../../utils/validate';
const propTypes = {
isValidUsername: PropTypes.bool,
submitNewUsername: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
username: PropTypes.string,
validateUsername: PropTypes.func.isRequired,
validating: PropTypes.bool
type UsernameProps = {
isValidUsername: boolean;
submitNewUsername: (name: string) => void;
t: (str: string, obj?: { username: string }) => string;
username: string;
validateUsername: (name: string) => void;
validating: boolean;
};
type UsernameState = {
isFormPristine: boolean;
formValue: string;
characterValidation: {
valid: boolean;
error: null | string;
};
submitClicked: boolean;
isUserNew: boolean;
};
const mapStateToProps = createSelector(
usernameValidationSelector,
({ isValidUsername, fetchState }) => ({
({
isValidUsername,
fetchState
}: {
isValidUsername: boolean;
fetchState: {
pending: boolean;
complete: boolean;
errored: boolean;
error: boolean | null;
};
}) => ({
isValidUsername,
validating: fetchState.pending
})
);
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
submitNewUsername,
@ -49,8 +74,9 @@ const mapDispatchToProps = dispatch =>
const hex = '[0-9a-f]';
const tempUserRegex = new RegExp(`^fcc${hex}{8}-(${hex}{4}-){3}${hex}{12}$`);
class UsernameSettings extends Component {
constructor(props) {
class UsernameSettings extends Component<UsernameProps, UsernameState> {
static displayName: string;
constructor(props: UsernameProps) {
super(props);
this.state = {
@ -66,13 +92,13 @@ class UsernameSettings extends Component {
this.validateFormInput = this.validateFormInput.bind(this);
}
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps: UsernameProps, prevState: UsernameState) {
const { username: prevUsername } = prevProps;
const { formValue: prevFormValue } = prevState;
const { username } = this.props;
const { formValue } = this.state;
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({
isFormPristine: username === formValue,
submitClicked: false,
@ -82,7 +108,7 @@ class UsernameSettings extends Component {
return null;
}
handleSubmit(e) {
handleSubmit(e: React.FormEvent) {
e.preventDefault();
const { submitNewUsername } = this.props;
const {
@ -95,10 +121,10 @@ class UsernameSettings extends Component {
);
}
handleChange(e) {
handleChange(e: React.FormEvent<HTMLInputElement>) {
e.preventDefault();
const { username, validateUsername } = this.props;
const newValue = e.target.value;
const newValue = (e.target as HTMLInputElement).value;
return this.setState(
{
formValue: newValue,
@ -113,11 +139,15 @@ class UsernameSettings extends Component {
);
}
validateFormInput(formValue) {
validateFormInput(formValue: string) {
return isValidUsername(formValue);
}
renderAlerts(validating, error, isValidUsername) {
renderAlerts(
validating: boolean,
error: string | null,
isValidUsername: boolean
) {
const { t } = this.props;
if (!validating && error) {
@ -203,7 +233,6 @@ class UsernameSettings extends Component {
}
UsernameSettings.displayName = 'UsernameSettings';
UsernameSettings.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,3 +1,6 @@
// eslint-disable-next-line import/unambiguous
declare module '@freecodecamp/react-bootstrap';
declare module '@freecodecamp/strip-comments';
declare module '@types/react-redux';
declare module '@types/validator';
declare module '@types/lodash-es';