diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/show-certification.tsx similarity index 80% rename from client/src/client-only-routes/ShowCertification.js rename to client/src/client-only-routes/show-certification.tsx index 7c2c8a8415..bf1b1bc0de 100644 --- a/client/src/client-only-routes/ShowCertification.js +++ b/client/src/client-only-routes/show-certification.tsx @@ -1,12 +1,11 @@ /* eslint-disable react/jsx-sort-props */ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; +import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap'; -import ShowProjectLinks from './ShowProjectLinks'; +import ShowProjectLinks from './show-project-links'; import FreeCodeCampLogo from '../assets/icons/FreeCodeCamp-logo'; // eslint-disable-next-line max-len import DonateForm from '../components/Donation/DonateForm'; @@ -23,62 +22,70 @@ import { userByNameSelector, fetchProfileForUser } from '../redux'; -import { certMap } from '../../src/resources/certAndProjectMap'; +import { certMap } from '../resources/certAndProjectMap'; import { createFlashMessage } from '../components/Flash/redux'; import standardErrorMessage from '../utils/standardErrorMessage'; import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage'; import { langCodes } from '../../../config/i18n/all-langs'; +// eslint-disable-next-line +// @ts-ignore import envData from '../../../config/env.json'; import RedirectHome from '../components/RedirectHome'; import { Loader, Spacer } from '../components/helpers'; import { isEmpty } from 'lodash-es'; -import { User } from '../redux/prop-types'; +import { UserType } from '../redux/prop-types'; -const { clientLocale } = envData; +const { clientLocale } = envData as { clientLocale: keyof typeof langCodes }; const localeCode = langCodes[clientLocale]; - -const propTypes = { - cert: PropTypes.shape({ - username: PropTypes.string, - name: PropTypes.string, - certName: PropTypes.string, - certTitle: PropTypes.string, - completionTime: PropTypes.number, - date: PropTypes.number - }), - certDashedName: PropTypes.string, - certSlug: PropTypes.string, - createFlashMessage: PropTypes.func.isRequired, - executeGA: PropTypes.func, - fetchProfileForUser: PropTypes.func, - fetchState: PropTypes.shape({ - pending: PropTypes.bool, - complete: PropTypes.bool, - errored: PropTypes.bool - }), - isDonating: PropTypes.bool, - isValidCert: PropTypes.bool, - location: PropTypes.shape({ - pathname: PropTypes.string - }), - showCert: PropTypes.func.isRequired, - signedInUserName: PropTypes.string, - user: User, - userFetchState: PropTypes.shape({ - complete: PropTypes.bool - }), - userFullName: PropTypes.string, - username: PropTypes.string +type CertType = { + username: string; + name: string; + certName: string; + certTitle: string; + completionTime: number; + date: number; }; +interface IShowCertificationProps { + cert: CertType; + certDashedName: string; + certSlug: string; + createFlashMessage: (payload: typeof standardErrorMessage) => void; + executeGA: (payload: Record) => void; + fetchProfileForUser: (username: string) => void; + fetchState: { + pending: boolean; + complete: boolean; + errored: boolean; + }; + isDonating: boolean; + isValidCert: boolean; + location: { + pathname: string; + }; + showCert: ({ + username, + certSlug + }: { + username: string; + certSlug: string; + }) => void; + signedInUserName: string; + user: UserType; + userFetchState: { + complete: boolean; + }; + userFullName: string; + username: string; +} -const requestedUserSelector = (state, { username = '' }) => - userByNameSelector(username.toLowerCase())(state); +const requestedUserSelector = (state: unknown, { username = '' }) => + userByNameSelector(username.toLowerCase())(state) as UserType; const validCertSlugs = certMap.map(cert => cert.certSlug); -const mapStateToProps = (state, props) => { +const mapStateToProps = (state: unknown, props: IShowCertificationProps) => { const isValidCert = validCertSlugs.some(slug => slug === props.certSlug); return createSelector( showCertSelector, @@ -87,7 +94,14 @@ const mapStateToProps = (state, props) => { userFetchStateSelector, isDonatingSelector, requestedUserSelector, - (cert, fetchState, signedInUserName, userFetchState, isDonating, user) => ({ + ( + cert: CertType, + fetchState: IShowCertificationProps['fetchState'], + signedInUserName: string, + userFetchState: IShowCertificationProps['userFetchState'], + isDonating: boolean, + user + ) => ({ cert, fetchState, isValidCert, @@ -99,13 +113,13 @@ const mapStateToProps = (state, props) => { ); }; -const mapDispatchToProps = dispatch => +const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators( { createFlashMessage, showCert, fetchProfileForUser, executeGA }, dispatch ); -const ShowCertification = props => { +const ShowCertification = (props: IShowCertificationProps): JSX.Element => { const { t } = useTranslation(); const [isDonationSubmitted, setIsDonationSubmitted] = useState(false); const [isDonationDisplayed, setIsDonationDisplayed] = useState(false); @@ -131,6 +145,7 @@ const ShowCertification = props => { } = props; if (!signedInUserName || signedInUserName !== username) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (isEmpty(user) && username) { fetchProfileForUser(username); } @@ -168,7 +183,11 @@ const ShowCertification = props => { setIsDonationClosed(true); }; - const handleProcessing = (duration, amount, action) => { + const handleProcessing = ( + duration: string, + amount: number, + action: string + ) => { props.executeGA({ type: 'event', data: { @@ -241,7 +260,7 @@ const ShowCertification = props => { ); - let donationSection = ( + const donationSection = (
{!isDonationSubmitted && ( @@ -327,12 +346,7 @@ const ShowCertification = props => {
- +

placeholder

{{ user: displayName }} @@ -376,6 +390,5 @@ const ShowCertification = props => { }; ShowCertification.displayName = 'ShowCertification'; -ShowCertification.propTypes = propTypes; export default connect(mapStateToProps, mapDispatchToProps)(ShowCertification); diff --git a/client/src/client-only-routes/ShowProfileOrFourOhFour.js b/client/src/client-only-routes/show-profile-or-four-oh-four.tsx similarity index 52% rename from client/src/client-only-routes/ShowProfileOrFourOhFour.js rename to client/src/client-only-routes/show-profile-or-four-oh-four.tsx index e164c727e4..e327f3d716 100644 --- a/client/src/client-only-routes/ShowProfileOrFourOhFour.js +++ b/client/src/client-only-routes/show-profile-or-four-oh-four.tsx @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { isEmpty } from 'lodash-es'; @@ -13,46 +12,58 @@ import { import FourOhFour from '../components/FourOhFour'; import Profile from '../components/profile/Profile'; import { isBrowser } from '../../utils/index'; +import { UserType } from '../redux/prop-types'; -const propTypes = { - fetchProfileForUser: PropTypes.func.isRequired, - isSessionUser: PropTypes.bool, - maybeUser: PropTypes.string, - requestedUser: PropTypes.shape({ - username: PropTypes.string, - profileUI: PropTypes.object - }), - showLoading: PropTypes.bool -}; +interface IShowProfileOrFourOhFourProps { + fetchProfileForUser: (username: string) => void; + fetchState: { + pending: boolean; + complete: boolean; + errored: boolean; + }; + isSessionUser: boolean; + maybeUser: string; + requestedUser: UserType; + showLoading: boolean; +} const createRequestedUserSelector = () => - (state, { maybeUser = '' }) => - userByNameSelector(maybeUser.toLowerCase())(state); + (state: unknown, { maybeUser = '' }) => + userByNameSelector(maybeUser.toLowerCase())(state) as string; const createIsSessionUserSelector = () => - (state, { maybeUser = '' }) => + (state: unknown, { maybeUser = '' }) => maybeUser.toLowerCase() === usernameSelector(state); -const makeMapStateToProps = () => (state, props) => { - const requestedUserSelector = createRequestedUserSelector(); - const isSessionUserSelector = createIsSessionUserSelector(); - const fetchState = userProfileFetchStateSelector(state, props); - return { - requestedUser: requestedUserSelector(state, props), - isSessionUser: isSessionUserSelector(state, props), - showLoading: fetchState.pending, - fetchState +const makeMapStateToProps = + () => (state: unknown, props: IShowProfileOrFourOhFourProps) => { + const requestedUserSelector = createRequestedUserSelector(); + const isSessionUserSelector = createIsSessionUserSelector(); + const fetchState = userProfileFetchStateSelector( + state + ) as IShowProfileOrFourOhFourProps['fetchState']; + return { + requestedUser: requestedUserSelector( + state, + props + ) as IShowProfileOrFourOhFourProps['requestedUser'], + isSessionUser: isSessionUserSelector(state, props), + showLoading: fetchState.pending, + fetchState + }; }; -}; -const mapDispatchToProps = { +const mapDispatchToProps: { + fetchProfileForUser: IShowProfileOrFourOhFourProps['fetchProfileForUser']; +} = { fetchProfileForUser }; -class ShowProfileOrFourOhFour extends Component { +class ShowProfileOrFourOhFour extends Component { componentDidMount() { const { requestedUser, maybeUser, fetchProfileForUser } = this.props; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (isEmpty(requestedUser)) { fetchProfileForUser(maybeUser); } @@ -64,6 +75,7 @@ class ShowProfileOrFourOhFour extends Component { } const { isSessionUser, requestedUser, showLoading } = this.props; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (isEmpty(requestedUser)) { if (showLoading) { // We don't know if /:maybeUser is a user or not, we will show @@ -78,13 +90,17 @@ class ShowProfileOrFourOhFour extends Component { // We have a response from the API, and we have some state in the // store for /:maybeUser, we now handover rendering to the Profile component + // eslint-disable-next-line + // @ts-ignore TODO: sort out whether user.portfolio is an array or obj. lit. return ; } } +// eslint-disable-next-line +// @ts-ignore ShowProfileOrFourOhFour.displayName = 'ShowProfileOrFourOhFour'; -ShowProfileOrFourOhFour.propTypes = propTypes; +// eslint-disable-next-line @typescript-eslint/no-unsafe-call export default connect( makeMapStateToProps, mapDispatchToProps diff --git a/client/src/client-only-routes/ShowProjectLinks.js b/client/src/client-only-routes/show-project-links.tsx similarity index 70% rename from client/src/client-only-routes/ShowProjectLinks.js rename to client/src/client-only-routes/show-project-links.tsx index 6abf6599b4..2945caa7c6 100644 --- a/client/src/client-only-routes/ShowProjectLinks.js +++ b/client/src/client-only-routes/show-project-links.tsx @@ -1,6 +1,5 @@ /* eslint-disable react/jsx-sort-props */ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import '../components/layouts/project-links.css'; import { maybeUrlRE } from '../utils'; import { Spacer, Link } from '../components/helpers'; @@ -8,69 +7,63 @@ import { projectMap, legacyProjectMap } from '../resources/certAndProjectMap'; import ProjectModal from '../components/SolutionViewer/ProjectModal'; import { find, first } from 'lodash-es'; import { Trans, useTranslation } from 'react-i18next'; +import { + ChallengeFileType, + CompletedChallenge, + UserType +} from '../redux/prop-types'; -const propTypes = { - certName: PropTypes.string, - name: PropTypes.string, - user: PropTypes.shape({ - completedChallenges: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - solution: PropTypes.string, - githubLink: PropTypes.string, - files: PropTypes.arrayOf( - PropTypes.shape({ - contents: PropTypes.string, - ext: PropTypes.string, - key: PropTypes.string, - name: PropTypes.string, - path: PropTypes.string - }) - ) - }) - ), - username: PropTypes.string - }) +interface IShowProjectLinksProps { + certName: string; + name: string; + user: UserType; +} + +type SolutionStateType = { + projectTitle: string; + challengeFiles: ChallengeFileType[] | null; + solution: null | string; + isOpen: boolean; }; -const initSolutionState = { +const initSolutionState: SolutionStateType = { projectTitle: '', - files: null, + challengeFiles: null, solution: null, isOpen: false }; -const ShowProjectLinks = props => { +const ShowProjectLinks = (props: IShowProjectLinksProps): JSX.Element => { const [solutionState, setSolutionState] = useState(initSolutionState); const handleSolutionModalHide = () => setSolutionState(initSolutionState); const { t } = useTranslation(); - const getProjectSolution = (projectId, projectTitle) => { + const getProjectSolution = (projectId: string, projectTitle: string) => { const { user: { completedChallenges } } = props; - + // eslint-disable-next-line @typescript-eslint/no-unsafe-call const completedProject = find( completedChallenges, ({ id }) => projectId === id - ); + ) as CompletedChallenge; if (!completedProject) { return null; } - const { solution, githubLink, files } = completedProject; + const { solution, githubLink, challengeFiles } = completedProject; const onClickHandler = () => setSolutionState({ projectTitle, - files, + challengeFiles, solution, isOpen: true }); - if (files && files.length) { + if (challengeFiles) { return ( + {/* eslint-disable @typescript-eslint/unbound-method */} @@ -151,9 +162,10 @@ class ShowUser extends Component { } } +// @ts-expect-error Config might need to be remedied, or component transformed into F.C. ShowUser.displayName = 'ShowUser'; -ShowUser.propTypes = propTypes; export default withTranslation()( + // eslint-disable-next-line @typescript-eslint/no-unsafe-call connect(mapStateToProps, mapDispatchToProps)(ShowUser) ); diff --git a/client/src/pages/404.js b/client/src/pages/404.js index fd8dcff09a..d804c3a98a 100644 --- a/client/src/pages/404.js +++ b/client/src/pages/404.js @@ -4,7 +4,7 @@ import { withPrefix } from 'gatsby'; import FourOhFour from '../components/FourOhFour'; /* eslint-disable max-len */ -import ShowProfileOrFourOhFour from '../client-only-routes/ShowProfileOrFourOhFour'; +import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four'; /* eslint-enable max-len */ function FourOhFourPage() { diff --git a/client/src/pages/certification.js b/client/src/pages/certification.js index 236a42ee13..1cfbf09371 100644 --- a/client/src/pages/certification.js +++ b/client/src/pages/certification.js @@ -3,7 +3,7 @@ import { Router } from '@reach/router'; import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; -import ShowCertification from '../client-only-routes/ShowCertification'; +import ShowCertification from '../client-only-routes/show-certification'; import './certification.css'; diff --git a/client/src/pages/settings.js b/client/src/pages/settings.js index e1170f2f07..c89a0d47e0 100644 --- a/client/src/pages/settings.js +++ b/client/src/pages/settings.js @@ -3,7 +3,7 @@ import { Router } from '@reach/router'; import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; -import ShowSettings from '../client-only-routes/ShowSettings'; +import ShowSettings from '../client-only-routes/show-settings'; function Settings() { return ( diff --git a/client/src/pages/unsubscribed.js b/client/src/pages/unsubscribed.js index e0a3ea7305..2670a37174 100644 --- a/client/src/pages/unsubscribed.js +++ b/client/src/pages/unsubscribed.js @@ -3,7 +3,7 @@ import { Router } from '@reach/router'; import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; -import ShowUnsubscribed from '../client-only-routes/ShowUnsubscribed'; +import ShowUnsubscribed from '../client-only-routes/show-unsubscribed'; function Unsubscribed() { return ( diff --git a/client/src/pages/user.js b/client/src/pages/user.js index cb78847438..1717ae3c27 100644 --- a/client/src/pages/user.js +++ b/client/src/pages/user.js @@ -3,7 +3,7 @@ import { Router } from '@reach/router'; import { withPrefix } from 'gatsby'; import RedirectHome from '../components/RedirectHome'; -import ShowUser from '../client-only-routes/ShowUser'; +import ShowUser from '../client-only-routes/show-user'; function User() { return ( diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 305dfcc9dc..38fff44057 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -215,6 +215,7 @@ export const shouldRequestDonationSelector = state => { export const userByNameSelector = username => state => { const { user } = state[ns]; + // TODO: Why return a string or empty objet literal? return username in user ? user[username] : {}; };