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 {
{
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ classForValue={(value: any) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!value || value.count < 1) return 'color-empty';
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value.count < 4) return 'color-scale-1';
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value.count < 8) return 'color-scale-2';
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value.count >= 8) return 'color-scale-a-lot';
return 'color-empty';
}}
endDate={endOfCalendar}
startDate={startOfCalendar}
- tooltipDataAttrs={value => {
- const dateFormatted =
+ tooltipDataAttrs={(value: { count: number; date: Date }) => {
+ const dateFormatted: string =
value && value.date
? value.date.toLocaleDateString([localeCode, 'en-US'], {
year: 'numeric',
@@ -156,10 +185,9 @@ class HeatMapInner extends Component {
}
}
-HeatMapInner.propTypes = innerPropTypes;
-
-const HeatMap = props => {
+const HeatMap = (props: IHeatMapProps): JSX.Element => {
const { t } = useTranslation();
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { calendar } = props;
/**
@@ -168,13 +196,14 @@ const HeatMap = props => {
*/
// create array of timestamps and turn into milliseconds
- const timestamps = Object.keys(calendar).map(stamp => stamp * 1000);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const timestamps = Object.keys(calendar).map((stamp: any) => stamp * 1000);
const startOfTimestamps = startOfDay(new Date(timestamps[0]));
let endOfCalendar = startOfDay(Date.now());
let startOfCalendar;
// creates pages for heatmap
- let pages = [];
+ const pages: IPageData[] = [];
do {
startOfCalendar = addDays(addMonths(endOfCalendar, -6), 1);
@@ -191,7 +220,7 @@ const HeatMap = props => {
pages.reverse();
- let calendarData = [];
+ const calendarData: ICalendarData[] = [];
let dayCounter = pages[0].startOfCalendar;
// create an object for each day of the calendar period
@@ -258,6 +287,5 @@ const HeatMap = props => {
};
HeatMap.displayName = 'HeatMap';
-HeatMap.propTypes = propTypes;
export default HeatMap;
diff --git a/client/src/components/profile/components/Portfolio.js b/client/src/components/profile/components/Portfolio.tsx
similarity index 76%
rename from client/src/components/profile/components/Portfolio.js
rename to client/src/components/profile/components/Portfolio.tsx
index 4b371ca29b..288d45ba27 100644
--- a/client/src/components/profile/components/Portfolio.js
+++ b/client/src/components/profile/components/Portfolio.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import PropTypes from 'prop-types';
import { Media } from '@freecodecamp/react-bootstrap';
import { useTranslation } from 'react-i18next';
@@ -7,19 +6,19 @@ import { FullWidthRow } from '../../helpers';
import './portfolio.css';
-const propTypes = {
- portfolio: PropTypes.arrayOf(
- PropTypes.shape({
- description: PropTypes.string,
- id: PropTypes.string,
- image: PropTypes.string,
- title: PropTypes.string,
- url: PropTypes.string
- })
- )
-};
+interface IPortfolioData {
+ description: string;
+ id: string;
+ image: string;
+ title: string;
+ url: string;
+}
-function Portfolio({ portfolio = [] }) {
+interface IPortfolioProps {
+ portfolio: IPortfolioData[];
+}
+
+function Portfolio({ portfolio = [] }: IPortfolioProps): JSX.Element | null {
const { t } = useTranslation();
if (!portfolio.length) {
return null;
@@ -54,6 +53,5 @@ function Portfolio({ portfolio = [] }) {
}
Portfolio.displayName = 'Portfolio';
-Portfolio.propTypes = propTypes;
export default Portfolio;
diff --git a/client/src/components/profile/components/SocialIcons.js b/client/src/components/profile/components/SocialIcons.tsx
similarity index 78%
rename from client/src/components/profile/components/SocialIcons.js
rename to client/src/components/profile/components/SocialIcons.tsx
index a250d5ab5d..5d4ba9feac 100644
--- a/client/src/components/profile/components/SocialIcons.js
+++ b/client/src/components/profile/components/SocialIcons.tsx
@@ -11,21 +11,21 @@ import { faLink } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'react-i18next';
import './social-icons.css';
-const propTypes = {
- email: PropTypes.string,
- githubProfile: PropTypes.string,
- isGithub: PropTypes.bool,
- isLinkedIn: PropTypes.bool,
- isTwitter: PropTypes.bool,
- isWebsite: PropTypes.bool,
- linkedin: PropTypes.string,
- show: PropTypes.bool,
- twitter: PropTypes.string,
- username: PropTypes.string,
- website: PropTypes.string
-};
+interface ISocialIconsProps {
+ email?: string;
+ githubProfile: string;
+ isGithub: boolean;
+ isLinkedIn: boolean;
+ isTwitter: boolean;
+ isWebsite: boolean;
+ linkedin: string;
+ show?: boolean;
+ twitter: string;
+ username: string;
+ website: string;
+}
-function LinkedInIcon(linkedIn, username) {
+function LinkedInIcon(linkedIn: string, username: string): JSX.Element {
const { t } = useTranslation();
return (
{
+ // @ts-ignore
useStaticQuery.mockImplementationOnce(() => ({
allChallengeNode: {
edges: [
@@ -41,6 +42,7 @@ beforeEach(() => {
describe('', () => {
it('Render button when only solution is present', () => {
+ // @ts-ignore
const { container } = render();
expect(
@@ -49,11 +51,13 @@ describe('', () => {
});
it('Render button when both githubLink and solution is present', () => {
+ // @ts-ignore
const { container } = render();
const linkList = container.querySelector(
'#dropdown-for-5e4f5c4b570f7e3a4949899f + ul'
);
+ // @ts-ignore
const links = linkList.querySelectorAll('a');
expect(links[0]).toHaveAttribute(
@@ -68,6 +72,7 @@ describe('', () => {
});
it('rendering the correct button when files is present', () => {
+ // @ts-ignore
const { getByText } = render();
const button = getByText('buttons.show-code');
diff --git a/client/src/components/profile/components/TimeLine.js b/client/src/components/profile/components/TimeLine.tsx
similarity index 79%
rename from client/src/components/profile/components/TimeLine.js
rename to client/src/components/profile/components/TimeLine.tsx
index 071d8f1e2e..8f47bf8a8b 100644
--- a/client/src/components/profile/components/TimeLine.js
+++ b/client/src/components/profile/components/TimeLine.tsx
@@ -1,5 +1,4 @@
import React, { Component, useMemo } from 'react';
-import PropTypes from 'prop-types';
import { reverse, sortBy } from 'lodash-es';
import {
Button,
@@ -9,7 +8,7 @@ import {
MenuItem
} from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby';
-import { withTranslation } from 'react-i18next';
+import { TFunction, withTranslation } from 'react-i18next';
import './timeline.css';
import TimelinePagination from './TimelinePagination';
@@ -26,63 +25,75 @@ import { maybeUrlRE } from '../../../utils';
import CertificationIcon from '../../../assets/icons/certification-icon';
import { langCodes } from '../../../../../config/i18n/all-langs';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
import envData from '../../../../../config/env.json';
-const SolutionViewer = Loadable(() =>
- import('../../SolutionViewer/SolutionViewer')
+const SolutionViewer = Loadable(
+ () =>
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ import('../../SolutionViewer/SolutionViewer')
);
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { clientLocale } = envData;
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/ban-ts-comment
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
const localeCode = langCodes[clientLocale];
// Items per page in timeline.
const ITEMS_PER_PAGE = 15;
-const propTypes = {
- completedMap: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- completedDate: PropTypes.number,
- challengeType: PropTypes.number,
- solution: PropTypes.string,
- files: PropTypes.arrayOf(
- PropTypes.shape({
- ext: PropTypes.string,
- contents: PropTypes.string
- })
- )
- })
- ),
- t: PropTypes.func.isRequired,
- username: PropTypes.string
-};
+interface ICompletedMap {
+ id: string;
+ completedDate: number;
+ challengeType: number;
+ solution: string;
+ files: IFile[];
+ githubLink: string;
+}
-const innerPropTypes = {
- ...propTypes,
- idToNameMap: PropTypes.objectOf(
- PropTypes.shape({
- challengePath: PropTypes.string,
- challengeTitle: PropTypes.string
- })
- ).isRequired,
- sortedTimeline: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- completedDate: PropTypes.number,
- files: PropTypes.arrayOf(
- PropTypes.shape({
- ext: PropTypes.string,
- contents: PropTypes.string
- })
- )
- })
- ).isRequired,
- totalPages: PropTypes.number.isRequired
-};
+interface ITimelineProps {
+ completedMap: ICompletedMap[];
+ t: TFunction<'translation'>;
+ username: string;
+}
-class TimelineInner extends Component {
- constructor(props) {
+interface IFile {
+ ext: string;
+ contents: string;
+}
+
+interface ISortedTimeline {
+ id: string;
+ completedDate: number;
+ files: IFile[];
+ githubLink: string;
+ solution: string;
+}
+
+interface ITimelineInnerProps extends ITimelineProps {
+ idToNameMap: Map;
+ sortedTimeline: ISortedTimeline[];
+ totalPages: number;
+}
+
+interface ITimeLineInnerState {
+ solutionToView: string | null;
+ solutionOpen: boolean;
+ pageNo: number;
+ solution: string | null;
+ files: IFile[] | null;
+}
+
+class TimelineInner extends Component<
+ ITimelineInnerProps,
+ ITimeLineInnerState
+> {
+ constructor(props: ITimelineInnerProps) {
super(props);
this.state = {
@@ -103,7 +114,12 @@ class TimelineInner extends Component {
this.renderViewButton = this.renderViewButton.bind(this);
}
- renderViewButton(id, files, githubLink, solution) {
+ renderViewButton(
+ id: string,
+ files: IFile[],
+ githubLink: string,
+ solution: string
+ ): React.ReactNode {
const { t } = this.props;
if (files && files.length) {
return (
@@ -165,10 +181,12 @@ class TimelineInner extends Component {
}
}
- renderCompletion(completed) {
+ renderCompletion(completed: ISortedTimeline): JSX.Element {
const { idToNameMap, username } = this.props;
const { id, files, githubLink, solution } = completed;
const completedDate = new Date(completed.completedDate);
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
return (
@@ -199,7 +217,7 @@ class TimelineInner extends Component {
);
}
- viewSolution(id, solution, files) {
+ viewSolution(id: string, solution: string, files: IFile[]): void {
this.setState(state => ({
...state,
solutionToView: id,
@@ -286,13 +304,17 @@ class TimelineInner extends Component {
{`${username}'s Solution to ${
+ // @ts-ignore
idToNameMap.get(id).challengeTitle
}`}
+ {/* @ts-ignore */}
@@ -316,9 +338,7 @@ class TimelineInner extends Component {
}
}
-TimelineInner.propTypes = innerPropTypes;
-
-function useIdToNameMap() {
+function useIdToNameMap(): Map {
const {
allChallengeNode: { edges }
} = useStaticQuery(graphql`
@@ -337,7 +357,7 @@ function useIdToNameMap() {
}
`);
const idToNameMap = new Map();
- for (let id of getCertIds()) {
+ for (const id of getCertIds()) {
idToNameMap.set(id, {
challengeTitle: `${getTitleFromId(id)} Certification`,
certPath: getPathFromID(id)
@@ -346,8 +366,11 @@ function useIdToNameMap() {
edges.forEach(
({
node: {
+ // @ts-ignore
id,
+ // @ts-ignore
title,
+ // @ts-ignore
fields: { slug }
}
}) => {
@@ -357,7 +380,7 @@ function useIdToNameMap() {
return idToNameMap;
}
-const Timeline = props => {
+const Timeline = (props: ITimelineProps): JSX.Element => {
const idToNameMap = useIdToNameMap();
const { completedMap } = props;
// Get the sorted timeline along with total page count.
@@ -380,8 +403,6 @@ const Timeline = props => {
);
};
-Timeline.propTypes = propTypes;
-
Timeline.displayName = 'Timeline';
export default withTranslation()(Timeline);
diff --git a/client/src/components/profile/components/TimelinePagination.js b/client/src/components/profile/components/TimelinePagination.tsx
similarity index 87%
rename from client/src/components/profile/components/TimelinePagination.js
rename to client/src/components/profile/components/TimelinePagination.tsx
index 911ec11256..8ff0aaaff9 100644
--- a/client/src/components/profile/components/TimelinePagination.js
+++ b/client/src/components/profile/components/TimelinePagination.tsx
@@ -3,7 +3,16 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
-const TimelinePagination = props => {
+interface ITimelinePaginationProps {
+ firstPage: () => void;
+ lastPage: () => void;
+ nextPage: () => void;
+ pageNo: number;
+ prevPage: () => void;
+ totalPages: number;
+}
+
+const TimelinePagination = (props: ITimelinePaginationProps): JSX.Element => {
const { pageNo, totalPages, firstPage, prevPage, nextPage, lastPage } = props;
const { t } = useTranslation();
@@ -81,13 +90,4 @@ const TimelinePagination = props => {
);
};
-TimelinePagination.propTypes = {
- firstPage: PropTypes.func.isRequired,
- lastPage: PropTypes.func.isRequired,
- nextPage: PropTypes.func.isRequired,
- pageNo: PropTypes.number.isRequired,
- prevPage: PropTypes.func.isRequired,
- totalPages: PropTypes.number.isRequired
-};
-
export default TimelinePagination;
diff --git a/package-lock.json b/package-lock.json
index ede0ba7687..3f9d78f49b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7372,6 +7372,15 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/loadable__component": {
+ "version": "5.13.3",
+ "resolved": "https://registry.npmjs.org/@types/loadable__component/-/loadable__component-5.13.3.tgz",
+ "integrity": "sha512-nkRRYpKxspH9yiYGSvyfM7GX1m57B9AVMrVEWY7Hlv9ueK2XCu39M5QyBTDHXcj5qixdD+MyAmNgoy2dEDxGLQ==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -7408,6 +7417,29 @@
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
"dev": true
},
+ "@types/prop-types": {
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
+ "dev": true
+ },
+ "@types/react": {
+ "version": "17.0.9",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.9.tgz",
+ "integrity": "sha512-2Cw7FvevpJxQrCb+k5t6GH1KIvmadj5uBbjPaLlJB/nZWUj56e1ZqcD6zsoMFB47MsJUTFl9RJ132A7hb3QFJA==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
+ "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==",
+ "dev": true
+ },
"@types/sinonjs__fake-timers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
@@ -10508,6 +10540,12 @@
}
}
},
+ "csstype": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
+ "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
+ "dev": true
+ },
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
diff --git a/package.json b/package.json
index 3c5d162f55..12cfd1d146 100644
--- a/package.json
+++ b/package.json
@@ -142,4 +142,4 @@
"typescript": "4.3.4",
"webpack-bundle-analyzer": "4.4.2"
}
-}
+}
\ No newline at end of file