diff --git a/client/package-lock.json b/client/package-lock.json index f0c23540bb..327ad22323 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4227,6 +4227,36 @@ "@types/react": "*" } }, + "@types/react-helmet": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.1.tgz", + "integrity": "sha512-VmSCMz6jp/06DABoY60vQa++h1YFt0PfAI23llxBJHbowqFgLUL0dhS1AQeVPNqYfRp9LAfokrfWACTNeobOrg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-redux": { + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz", + "integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -21608,4 +21638,4 @@ "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==" } } -} +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 3bde2b726e..a92024ac78 100644 --- a/client/package.json +++ b/client/package.json @@ -160,4 +160,4 @@ "webpack": "5.41.1", "webpack-cli": "4.7.2" } -} +} \ No newline at end of file diff --git a/client/src/components/profile/Profile.test.js b/client/src/components/profile/Profile.test.tsx similarity index 80% rename from client/src/components/profile/Profile.test.js rename to client/src/components/profile/Profile.test.tsx index feeed78bc5..b18ce9f08a 100644 --- a/client/src/components/profile/Profile.test.js +++ b/client/src/components/profile/Profile.test.tsx @@ -16,7 +16,8 @@ const userProps = { showName: false, showPoints: false, showPortfolio: false, - showTimeLine: false + showTimeLine: false, + showDonation: false }, calendar: {}, streak: { @@ -40,8 +41,10 @@ const userProps = { twitter: 'string', username: 'string', website: 'string', - yearsTopContributor: [] + yearsTopContributor: [], + isDonating: false }, + // eslint-disable-next-line @typescript-eslint/no-empty-function navigate: () => {} }; @@ -54,10 +57,8 @@ describe('', () => { it('renders the report button on another persons profile', () => { const { getByText } = render(); - expect(getByText('buttons.flag-user')).toHaveAttribute( - 'href', - '/user/string/report-user' - ); + const reportButton: HTMLElement = getByText('buttons.flag-user'); + expect(reportButton).toHaveAttribute('href', '/user/string/report-user'); }); it('renders correctly', () => { diff --git a/client/src/components/profile/Profile.js b/client/src/components/profile/Profile.tsx similarity index 64% rename from client/src/components/profile/Profile.js rename to client/src/components/profile/Profile.tsx index 76f22cdda1..ab4d2f598e 100644 --- a/client/src/components/profile/Profile.js +++ b/client/src/components/profile/Profile.tsx @@ -1,56 +1,62 @@ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { Grid, Row } from '@freecodecamp/react-bootstrap'; import Helmet from 'react-helmet'; -import Link from '../helpers/link'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; -import { CurrentChallengeLink, FullWidthRow, Spacer } from '../helpers'; +import { CurrentChallengeLink, FullWidthRow, Link, Spacer } from '../helpers'; import Camper from './components/Camper'; import HeatMap from './components/HeatMap'; import Certifications from './components/Certifications'; import Portfolio from './components/Portfolio'; import Timeline from './components/TimeLine'; -const propTypes = { - isSessionUser: PropTypes.bool, - 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 - }), - calendar: PropTypes.object, - completedChallenges: PropTypes.array, - portfolio: PropTypes.array, - about: PropTypes.string, - githubProfile: PropTypes.string, - isGithub: PropTypes.bool, - isLinkedIn: PropTypes.bool, - isTwitter: PropTypes.bool, - isWebsite: PropTypes.bool, - joinDate: PropTypes.string, - linkedin: PropTypes.string, - location: PropTypes.string, - name: PropTypes.string, - picture: PropTypes.string, - points: PropTypes.number, - twitter: PropTypes.string, - username: PropTypes.string, - website: PropTypes.string, - yearsTopContributor: PropTypes.array, - isDonating: PropTypes.bool - }) -}; +interface IProfileProps { + isSessionUser: boolean; + user: { + profileUI: { + isLocked: boolean; + showAbout: boolean; + showCerts: boolean; + showDonation: boolean; + showHeatMap: boolean; + showLocation: boolean; + showName: boolean; + showPoints: boolean; + showPortfolio: boolean; + showTimeLine: boolean; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calendar: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + completedChallenges: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + portfolio: any[]; + about: string; + githubProfile: string; + isGithub: boolean; + isLinkedIn: boolean; + isTwitter: boolean; + isWebsite: boolean; + joinDate: string; + linkedin: string; + location: string; + name: string; + picture: string; + points: number; + twitter: string; + username: string; + website: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yearsTopContributor: any[]; + isDonating: boolean; + }; +} -function renderMessage(isSessionUser, username, t) { +function renderMessage( + isSessionUser: boolean, + username: string, + t: TFunction<'translation'> +): JSX.Element { return isSessionUser ? ( @@ -82,7 +88,7 @@ function renderMessage(isSessionUser, username, t) { ); } -function renderProfile(user) { +function renderProfile(user: IProfileProps['user']): JSX.Element { const { profileUI: { showAbout = false, @@ -95,6 +101,7 @@ function renderProfile(user) { showPortfolio = false, showTimeLine = false }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment calendar, completedChallenges, githubProfile, @@ -116,21 +123,20 @@ function renderProfile(user) { yearsTopContributor, isDonating } = user; - return ( + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */} {showHeatMap ? : null} {showCerts ? : null} {showPortfolio ? : null} @@ -149,7 +156,7 @@ function renderProfile(user) { ); } -function Profile({ user, isSessionUser }) { +function Profile({ user, isSessionUser }: IProfileProps): JSX.Element { const { t } = useTranslation(); const { profileUI: { isLocked = true }, @@ -180,6 +187,5 @@ function Profile({ user, isSessionUser }) { } Profile.displayName = 'Profile'; -Profile.propTypes = propTypes; export default Profile; diff --git a/client/src/components/profile/components/Camper.js b/client/src/components/profile/components/Camper.tsx similarity index 75% rename from client/src/components/profile/components/Camper.js rename to client/src/components/profile/components/Camper.tsx index cb267540d0..972f1d57bc 100644 --- a/client/src/components/profile/components/Camper.js +++ b/client/src/components/profile/components/Camper.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Col, Row } from '@freecodecamp/react-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faAward, - faHeart, - faCalendar + faCalendar, + faHeart } from '@fortawesome/free-solid-svg-icons'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import { AvatarRenderer } from '../../helpers'; import SocialIcons from './SocialIcons'; @@ -16,32 +15,35 @@ import Link from '../../helpers/link'; import './camper.css'; import { langCodes } from '../../../../../config/i18n/all-langs'; -import envData from '../../../../../config/env.json'; +import envData from '../../../../../config/env'; const { clientLocale } = envData; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const localeCode = langCodes[clientLocale]; -const propTypes = { - about: PropTypes.string, - githubProfile: PropTypes.string, - isDonating: PropTypes.bool, - isGithub: PropTypes.bool, - isLinkedIn: PropTypes.bool, - isTwitter: PropTypes.bool, - isWebsite: PropTypes.bool, - joinDate: PropTypes.string, - linkedin: PropTypes.string, - location: PropTypes.string, - name: PropTypes.string, - picture: PropTypes.string, - points: PropTypes.number, - twitter: PropTypes.string, - username: PropTypes.string, - website: PropTypes.string, - yearsTopContributor: PropTypes.array -}; +interface ICamperProps { + about: string; + githubProfile: string; + isDonating: boolean; + isGithub: boolean; + isLinkedIn: boolean; + isTwitter: boolean; + isWebsite: boolean; + joinDate: string; + linkedin: string; + location: string; + name: string; + picture: string; + points: number | null; + twitter: string; + username: string; + website: string; + yearsTopContributor: string[]; +} -function joinArray(array, t) { +function joinArray(array: string[], t: TFunction<'translation'>): string { return array.reduce((string, item, index, array) => { if (string.length > 0) { if (index === array.length - 1) { @@ -55,9 +57,9 @@ function joinArray(array, t) { }); } -function parseDate(joinDate, t) { - joinDate = new Date(joinDate); - const date = joinDate.toLocaleString([localeCode, 'en-US'], { +function parseDate(joinDate: string, t: TFunction<'translation'>): string { + const convertedJoinDate = new Date(joinDate); + const date = convertedJoinDate.toLocaleString([localeCode, 'en-US'], { year: 'numeric', month: 'long' }); @@ -82,7 +84,7 @@ function Camper({ linkedin, twitter, website -}) { +}: ICamperProps): JSX.Element { const { t } = useTranslation(); return ( @@ -144,6 +146,5 @@ function Camper({ } Camper.displayName = 'Camper'; -Camper.propTypes = propTypes; export default Camper; diff --git a/client/src/components/profile/components/Certifications.js b/client/src/components/profile/components/Certifications.tsx similarity index 65% rename from client/src/components/profile/components/Certifications.js rename to client/src/components/profile/components/Certifications.tsx index 730476fca5..5aea83c00f 100644 --- a/client/src/components/profile/components/Certifications.js +++ b/client/src/components/profile/components/Certifications.tsx @@ -1,36 +1,52 @@ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; import { curry } from 'lodash-es'; import { createSelector } from 'reselect'; import { connect } from 'react-redux'; -import { Row, Col } from '@freecodecamp/react-bootstrap'; +import { Col, Row } from '@freecodecamp/react-bootstrap'; import { useTranslation } from 'react-i18next'; import { certificatesByNameSelector } from '../../../redux'; import { ButtonSpacer, FullWidthRow, Link, Spacer } from '../../helpers'; import './certifications.css'; -import { CurrentCertsType } from '../../../redux/prop-types'; -const mapStateToProps = (state, props) => +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mapStateToProps = (state: any, props: ICertificationProps) => createSelector( certificatesByNameSelector(props.username), - ({ hasModernCert, hasLegacyCert, currentCerts, legacyCerts }) => ({ + ({ + hasModernCert, + hasLegacyCert, + currentCerts, + legacyCerts + }: Pick< + ICertificationProps, + 'hasModernCert' | 'hasLegacyCert' | 'currentCerts' | 'legacyCerts' + >) => ({ hasModernCert, hasLegacyCert, currentCerts, legacyCerts }) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore )(state, props); -const propTypes = { - currentCerts: CurrentCertsType, - hasLegacyCert: PropTypes.bool, - hasModernCert: PropTypes.bool, - legacyCerts: CurrentCertsType, - username: PropTypes.string -}; +interface ICert { + show: boolean; + title: string; + certSlug: string; +} -function renderCertShow(username, cert) { +interface ICertificationProps { + currentCerts?: ICert[]; + hasLegacyCert?: boolean; + hasModernCert?: boolean; + legacyCerts?: ICert[]; + username: string; +} + +function renderCertShow(username: string, cert: ICert): React.ReactNode { return cert.show ? ( @@ -55,14 +71,14 @@ function Certificates({ hasLegacyCert, hasModernCert, username -}) { +}: ICertificationProps): JSX.Element { const { t } = useTranslation(); const renderCertShowWithUsername = curry(renderCertShow)(username); return (

{t('profile.fcc-certs')}


- {hasModernCert ? ( + {hasModernCert && currentCerts ? ( currentCerts.map(renderCertShowWithUsername) ) : (

{t('profile.no-certs')}

@@ -72,7 +88,7 @@ function Certificates({

{t('settings.headings.legacy-certs')}


- {legacyCerts.map(renderCertShowWithUsername)} + {legacyCerts && legacyCerts.map(renderCertShowWithUsername)} ) : null} @@ -81,7 +97,6 @@ function Certificates({ ); } -Certificates.propTypes = propTypes; Certificates.displayName = 'Certifications'; export default connect(mapStateToProps)(Certificates); diff --git a/client/src/components/profile/components/HeatMap.test.js b/client/src/components/profile/components/HeatMap.test.tsx similarity index 90% rename from client/src/components/profile/components/HeatMap.test.js rename to client/src/components/profile/components/HeatMap.test.tsx index 61fb0c4a84..b982fa7363 100644 --- a/client/src/components/profile/components/HeatMap.test.js +++ b/client/src/components/profile/components/HeatMap.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import HeatMap from './HeatMap'; +import MockInstance = jest.MockInstance; // offset is used to shift the dates so that the calendar renders (for testing // purposes only) the same way in each timezone. @@ -10,7 +11,7 @@ const date1 = 1580497504 + offset; const date2 = 1580597504 + offset; const date3 = 1580729769 + offset; -const props = { +const props: { calendar: { [key: number]: number } } = { calendar: {} }; @@ -18,7 +19,7 @@ props.calendar[date1] = 1; props.calendar[date2] = 1; props.calendar[date3] = 1; -let dateNowMockFn; +let dateNowMockFn: MockInstance; beforeEach(() => { dateNowMockFn = jest diff --git a/client/src/components/profile/components/HeatMap.js b/client/src/components/profile/components/HeatMap.tsx similarity index 75% rename from client/src/components/profile/components/HeatMap.js rename to client/src/components/profile/components/HeatMap.tsx index 689bc1633c..b1ff2b03d0 100644 --- a/client/src/components/profile/components/HeatMap.js +++ b/client/src/components/profile/components/HeatMap.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import CalendarHeatMap from '@freecodecamp/react-calendar-heatmap'; import { Row } from '@freecodecamp/react-bootstrap'; import ReactTooltip from 'react-tooltip'; @@ -7,7 +8,7 @@ import addDays from 'date-fns/addDays'; import addMonths from 'date-fns/addMonths'; import startOfDay from 'date-fns/startOfDay'; import isEqual from 'date-fns/isEqual'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import FullWidthRow from '../../helpers/full-width-row'; import Spacer from '../../helpers/spacer'; @@ -16,27 +17,48 @@ import '@freecodecamp/react-calendar-heatmap/dist/styles.css'; import './heatmap.css'; import { langCodes } from '../../../../../config/i18n/all-langs'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import envData from '../../../../../config/env.json'; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { clientLocale } = envData; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment const localeCode = langCodes[clientLocale]; -const propTypes = { - calendar: PropTypes.object -}; +interface IHeatMapProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calendar: any; +} -const innerPropTypes = { - calendarData: PropTypes.array, - currentStreak: PropTypes.number, - longestStreak: PropTypes.number, - pages: PropTypes.array, - points: PropTypes.number, - t: PropTypes.func.isRequired -}; +interface IPageData { + startOfCalendar: Date; + endOfCalendar: Date; +} -class HeatMapInner extends Component { - constructor(props) { +interface ICalendarData { + date: Date; + count: number; +} + +interface IHeatMapInnerProps { + calendarData: ICalendarData[]; + currentStreak: number; + longestStreak: number; + pages: IPageData[]; + points?: number; + t: TFunction<'translation'>; +} + +interface IHeatMapInnerState { + pageIndex: number; +} + +class HeatMapInner extends Component { + constructor(props: IHeatMapInnerProps) { super(props); this.state = { @@ -85,6 +107,7 @@ class HeatMapInner extends Component {