feat(client): ts-migrate client/src/components/profile (#42378)

* feat: rename Link, Spacer, Profile for typescript

* feaat: migrate Spacer to typescript

* feat: migrate Link to typescript

* feat: migrate Profile to typescript

* feat: migrate Profile test to typescript

* feat: rename Camper.s to Camper.tsx

* feat: migrate Camper to typescript

* feat: rename Certifications

* feat: migrate Certifications to typescript

* feat: rename HeatMap

* feat: migrate HeatMap to typescript

* feat: rename HeatMap.test.

* feat: convert HeatMap.test. to typescript

* feat: make some props optional in ICertificationProps

* feat: rename Portfolio

* feat: migrate Portfolio to typescript

* feat: rename and migrate SocialIcons

* feat: rename TimeLine

* feat: migrate TimeLine to typescript

* feat: rename TimeLine.test.

* feat: migrate TimeLine.test. to typescript

* feat: rename TimelinePagination

* feat: migrate TimelinePagination to typescript

* feat: clean up for typescript migration

Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Dripcoding
2021-06-25 08:22:37 -07:00
committed by Mrugesh Mohapatra
parent f15a55e2b4
commit da461bf09a
15 changed files with 376 additions and 233 deletions

View File

@ -4227,6 +4227,36 @@
"@types/react": "*" "@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": { "@types/react-transition-group": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", "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==" "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="
} }
} }
} }

View File

@ -160,4 +160,4 @@
"webpack": "5.41.1", "webpack": "5.41.1",
"webpack-cli": "4.7.2" "webpack-cli": "4.7.2"
} }
} }

View File

@ -16,7 +16,8 @@ const userProps = {
showName: false, showName: false,
showPoints: false, showPoints: false,
showPortfolio: false, showPortfolio: false,
showTimeLine: false showTimeLine: false,
showDonation: false
}, },
calendar: {}, calendar: {},
streak: { streak: {
@ -40,8 +41,10 @@ const userProps = {
twitter: 'string', twitter: 'string',
username: 'string', username: 'string',
website: 'string', website: 'string',
yearsTopContributor: [] yearsTopContributor: [],
isDonating: false
}, },
// eslint-disable-next-line @typescript-eslint/no-empty-function
navigate: () => {} navigate: () => {}
}; };
@ -54,10 +57,8 @@ describe('<Profile/>', () => {
it('renders the report button on another persons profile', () => { it('renders the report button on another persons profile', () => {
const { getByText } = render(<Profile {...notMyProfileProps} />); const { getByText } = render(<Profile {...notMyProfileProps} />);
expect(getByText('buttons.flag-user')).toHaveAttribute( const reportButton: HTMLElement = getByText('buttons.flag-user');
'href', expect(reportButton).toHaveAttribute('href', '/user/string/report-user');
'/user/string/report-user'
);
}); });
it('renders correctly', () => { it('renders correctly', () => {

View File

@ -1,56 +1,62 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Grid, Row } from '@freecodecamp/react-bootstrap'; import { Grid, Row } from '@freecodecamp/react-bootstrap';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import Link from '../helpers/link'; import { TFunction, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { CurrentChallengeLink, FullWidthRow, Spacer } from '../helpers'; import { CurrentChallengeLink, FullWidthRow, Link, Spacer } from '../helpers';
import Camper from './components/Camper'; import Camper from './components/Camper';
import HeatMap from './components/HeatMap'; import HeatMap from './components/HeatMap';
import Certifications from './components/Certifications'; import Certifications from './components/Certifications';
import Portfolio from './components/Portfolio'; import Portfolio from './components/Portfolio';
import Timeline from './components/TimeLine'; import Timeline from './components/TimeLine';
const propTypes = { interface IProfileProps {
isSessionUser: PropTypes.bool, isSessionUser: boolean;
user: PropTypes.shape({ user: {
profileUI: PropTypes.shape({ profileUI: {
isLocked: PropTypes.bool, isLocked: boolean;
showAbout: PropTypes.bool, showAbout: boolean;
showCerts: PropTypes.bool, showCerts: boolean;
showDonation: PropTypes.bool, showDonation: boolean;
showHeatMap: PropTypes.bool, showHeatMap: boolean;
showLocation: PropTypes.bool, showLocation: boolean;
showName: PropTypes.bool, showName: boolean;
showPoints: PropTypes.bool, showPoints: boolean;
showPortfolio: PropTypes.bool, showPortfolio: boolean;
showTimeLine: PropTypes.bool showTimeLine: boolean;
}), };
calendar: PropTypes.object, // eslint-disable-next-line @typescript-eslint/no-explicit-any
completedChallenges: PropTypes.array, calendar: any;
portfolio: PropTypes.array, // eslint-disable-next-line @typescript-eslint/no-explicit-any
about: PropTypes.string, completedChallenges: any[];
githubProfile: PropTypes.string, // eslint-disable-next-line @typescript-eslint/no-explicit-any
isGithub: PropTypes.bool, portfolio: any[];
isLinkedIn: PropTypes.bool, about: string;
isTwitter: PropTypes.bool, githubProfile: string;
isWebsite: PropTypes.bool, isGithub: boolean;
joinDate: PropTypes.string, isLinkedIn: boolean;
linkedin: PropTypes.string, isTwitter: boolean;
location: PropTypes.string, isWebsite: boolean;
name: PropTypes.string, joinDate: string;
picture: PropTypes.string, linkedin: string;
points: PropTypes.number, location: string;
twitter: PropTypes.string, name: string;
username: PropTypes.string, picture: string;
website: PropTypes.string, points: number;
yearsTopContributor: PropTypes.array, twitter: string;
isDonating: PropTypes.bool 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 ? ( return isSessionUser ? (
<Fragment> <Fragment>
<FullWidthRow> <FullWidthRow>
@ -82,7 +88,7 @@ function renderMessage(isSessionUser, username, t) {
); );
} }
function renderProfile(user) { function renderProfile(user: IProfileProps['user']): JSX.Element {
const { const {
profileUI: { profileUI: {
showAbout = false, showAbout = false,
@ -95,6 +101,7 @@ function renderProfile(user) {
showPortfolio = false, showPortfolio = false,
showTimeLine = false showTimeLine = false
}, },
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
calendar, calendar,
completedChallenges, completedChallenges,
githubProfile, githubProfile,
@ -116,21 +123,20 @@ function renderProfile(user) {
yearsTopContributor, yearsTopContributor,
isDonating isDonating
} = user; } = user;
return ( return (
<Fragment> <Fragment>
<Camper <Camper
about={showAbout ? about : null} about={about}
githubProfile={githubProfile} githubProfile={githubProfile}
isDonating={showDonation ? isDonating : null} isDonating={showDonation ? isDonating : false}
isGithub={isGithub} isGithub={isGithub}
isLinkedIn={isLinkedIn} isLinkedIn={isLinkedIn}
isTwitter={isTwitter} isTwitter={isTwitter}
isWebsite={isWebsite} isWebsite={isWebsite}
joinDate={showAbout ? joinDate : null} joinDate={showAbout ? joinDate : ''}
linkedin={linkedin} linkedin={linkedin}
location={showLocation ? location : null} location={showLocation ? location : ''}
name={showName ? name : null} name={showName ? name : ''}
picture={picture} picture={picture}
points={showPoints ? points : null} points={showPoints ? points : null}
twitter={twitter} twitter={twitter}
@ -138,6 +144,7 @@ function renderProfile(user) {
website={website} website={website}
yearsTopContributor={yearsTopContributor} yearsTopContributor={yearsTopContributor}
/> />
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
{showHeatMap ? <HeatMap calendar={calendar} /> : null} {showHeatMap ? <HeatMap calendar={calendar} /> : null}
{showCerts ? <Certifications username={username} /> : null} {showCerts ? <Certifications username={username} /> : null}
{showPortfolio ? <Portfolio portfolio={portfolio} /> : null} {showPortfolio ? <Portfolio portfolio={portfolio} /> : null}
@ -149,7 +156,7 @@ function renderProfile(user) {
); );
} }
function Profile({ user, isSessionUser }) { function Profile({ user, isSessionUser }: IProfileProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
profileUI: { isLocked = true }, profileUI: { isLocked = true },
@ -180,6 +187,5 @@ function Profile({ user, isSessionUser }) {
} }
Profile.displayName = 'Profile'; Profile.displayName = 'Profile';
Profile.propTypes = propTypes;
export default Profile; export default Profile;

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from '@freecodecamp/react-bootstrap'; import { Col, Row } from '@freecodecamp/react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { import {
faAward, faAward,
faHeart, faCalendar,
faCalendar faHeart
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'react-i18next'; import { TFunction, useTranslation } from 'react-i18next';
import { AvatarRenderer } from '../../helpers'; import { AvatarRenderer } from '../../helpers';
import SocialIcons from './SocialIcons'; import SocialIcons from './SocialIcons';
@ -16,32 +15,35 @@ import Link from '../../helpers/link';
import './camper.css'; import './camper.css';
import { langCodes } from '../../../../../config/i18n/all-langs'; import { langCodes } from '../../../../../config/i18n/all-langs';
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env';
const { clientLocale } = envData; 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 localeCode = langCodes[clientLocale];
const propTypes = { interface ICamperProps {
about: PropTypes.string, about: string;
githubProfile: PropTypes.string, githubProfile: string;
isDonating: PropTypes.bool, isDonating: boolean;
isGithub: PropTypes.bool, isGithub: boolean;
isLinkedIn: PropTypes.bool, isLinkedIn: boolean;
isTwitter: PropTypes.bool, isTwitter: boolean;
isWebsite: PropTypes.bool, isWebsite: boolean;
joinDate: PropTypes.string, joinDate: string;
linkedin: PropTypes.string, linkedin: string;
location: PropTypes.string, location: string;
name: PropTypes.string, name: string;
picture: PropTypes.string, picture: string;
points: PropTypes.number, points: number | null;
twitter: PropTypes.string, twitter: string;
username: PropTypes.string, username: string;
website: PropTypes.string, website: string;
yearsTopContributor: PropTypes.array yearsTopContributor: string[];
}; }
function joinArray(array, t) { function joinArray(array: string[], t: TFunction<'translation'>): string {
return array.reduce((string, item, index, array) => { return array.reduce((string, item, index, array) => {
if (string.length > 0) { if (string.length > 0) {
if (index === array.length - 1) { if (index === array.length - 1) {
@ -55,9 +57,9 @@ function joinArray(array, t) {
}); });
} }
function parseDate(joinDate, t) { function parseDate(joinDate: string, t: TFunction<'translation'>): string {
joinDate = new Date(joinDate); const convertedJoinDate = new Date(joinDate);
const date = joinDate.toLocaleString([localeCode, 'en-US'], { const date = convertedJoinDate.toLocaleString([localeCode, 'en-US'], {
year: 'numeric', year: 'numeric',
month: 'long' month: 'long'
}); });
@ -82,7 +84,7 @@ function Camper({
linkedin, linkedin,
twitter, twitter,
website website
}) { }: ICamperProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -144,6 +146,5 @@ function Camper({
} }
Camper.displayName = 'Camper'; Camper.displayName = 'Camper';
Camper.propTypes = propTypes;
export default Camper; export default Camper;

View File

@ -1,36 +1,52 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { curry } from 'lodash-es'; import { curry } from 'lodash-es';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { connect } from 'react-redux'; 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 { useTranslation } from 'react-i18next';
import { certificatesByNameSelector } from '../../../redux'; import { certificatesByNameSelector } from '../../../redux';
import { ButtonSpacer, FullWidthRow, Link, Spacer } from '../../helpers'; import { ButtonSpacer, FullWidthRow, Link, Spacer } from '../../helpers';
import './certifications.css'; 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( createSelector(
certificatesByNameSelector(props.username), certificatesByNameSelector(props.username),
({ hasModernCert, hasLegacyCert, currentCerts, legacyCerts }) => ({ ({
hasModernCert,
hasLegacyCert,
currentCerts,
legacyCerts
}: Pick<
ICertificationProps,
'hasModernCert' | 'hasLegacyCert' | 'currentCerts' | 'legacyCerts'
>) => ({
hasModernCert, hasModernCert,
hasLegacyCert, hasLegacyCert,
currentCerts, currentCerts,
legacyCerts legacyCerts
}) })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
)(state, props); )(state, props);
const propTypes = { interface ICert {
currentCerts: CurrentCertsType, show: boolean;
hasLegacyCert: PropTypes.bool, title: string;
hasModernCert: PropTypes.bool, certSlug: string;
legacyCerts: CurrentCertsType, }
username: PropTypes.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 ? ( return cert.show ? (
<Fragment key={cert.title}> <Fragment key={cert.title}>
<Row> <Row>
@ -55,14 +71,14 @@ function Certificates({
hasLegacyCert, hasLegacyCert,
hasModernCert, hasModernCert,
username username
}) { }: ICertificationProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const renderCertShowWithUsername = curry(renderCertShow)(username); const renderCertShowWithUsername = curry(renderCertShow)(username);
return ( return (
<FullWidthRow className='certifications'> <FullWidthRow className='certifications'>
<h2 className='text-center'>{t('profile.fcc-certs')}</h2> <h2 className='text-center'>{t('profile.fcc-certs')}</h2>
<br /> <br />
{hasModernCert ? ( {hasModernCert && currentCerts ? (
currentCerts.map(renderCertShowWithUsername) currentCerts.map(renderCertShowWithUsername)
) : ( ) : (
<p className='text-center'>{t('profile.no-certs')}</p> <p className='text-center'>{t('profile.no-certs')}</p>
@ -72,7 +88,7 @@ function Certificates({
<br /> <br />
<h3 className='text-center'>{t('settings.headings.legacy-certs')}</h3> <h3 className='text-center'>{t('settings.headings.legacy-certs')}</h3>
<br /> <br />
{legacyCerts.map(renderCertShowWithUsername)} {legacyCerts && legacyCerts.map(renderCertShowWithUsername)}
<Spacer size={2} /> <Spacer size={2} />
</div> </div>
) : null} ) : null}
@ -81,7 +97,6 @@ function Certificates({
); );
} }
Certificates.propTypes = propTypes;
Certificates.displayName = 'Certifications'; Certificates.displayName = 'Certifications';
export default connect(mapStateToProps)(Certificates); export default connect(mapStateToProps)(Certificates);

View File

@ -2,6 +2,7 @@ import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import HeatMap from './HeatMap'; import HeatMap from './HeatMap';
import MockInstance = jest.MockInstance;
// offset is used to shift the dates so that the calendar renders (for testing // offset is used to shift the dates so that the calendar renders (for testing
// purposes only) the same way in each timezone. // purposes only) the same way in each timezone.
@ -10,7 +11,7 @@ const date1 = 1580497504 + offset;
const date2 = 1580597504 + offset; const date2 = 1580597504 + offset;
const date3 = 1580729769 + offset; const date3 = 1580729769 + offset;
const props = { const props: { calendar: { [key: number]: number } } = {
calendar: {} calendar: {}
}; };
@ -18,7 +19,7 @@ props.calendar[date1] = 1;
props.calendar[date2] = 1; props.calendar[date2] = 1;
props.calendar[date3] = 1; props.calendar[date3] = 1;
let dateNowMockFn; let dateNowMockFn: MockInstance<any, any>;
beforeEach(() => { beforeEach(() => {
dateNowMockFn = jest dateNowMockFn = jest

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import CalendarHeatMap from '@freecodecamp/react-calendar-heatmap'; import CalendarHeatMap from '@freecodecamp/react-calendar-heatmap';
import { Row } from '@freecodecamp/react-bootstrap'; import { Row } from '@freecodecamp/react-bootstrap';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
@ -7,7 +8,7 @@ import addDays from 'date-fns/addDays';
import addMonths from 'date-fns/addMonths'; import addMonths from 'date-fns/addMonths';
import startOfDay from 'date-fns/startOfDay'; import startOfDay from 'date-fns/startOfDay';
import isEqual from 'date-fns/isEqual'; 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 FullWidthRow from '../../helpers/full-width-row';
import Spacer from '../../helpers/spacer'; import Spacer from '../../helpers/spacer';
@ -16,27 +17,48 @@ import '@freecodecamp/react-calendar-heatmap/dist/styles.css';
import './heatmap.css'; import './heatmap.css';
import { langCodes } from '../../../../../config/i18n/all-langs'; import { langCodes } from '../../../../../config/i18n/all-langs';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { clientLocale } = envData; 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 localeCode = langCodes[clientLocale];
const propTypes = { interface IHeatMapProps {
calendar: PropTypes.object // eslint-disable-next-line @typescript-eslint/no-explicit-any
}; calendar: any;
}
const innerPropTypes = { interface IPageData {
calendarData: PropTypes.array, startOfCalendar: Date;
currentStreak: PropTypes.number, endOfCalendar: Date;
longestStreak: PropTypes.number, }
pages: PropTypes.array,
points: PropTypes.number,
t: PropTypes.func.isRequired
};
class HeatMapInner extends Component { interface ICalendarData {
constructor(props) { 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<IHeatMapInnerProps, IHeatMapInnerState> {
constructor(props: IHeatMapInnerProps) {
super(props); super(props);
this.state = { this.state = {
@ -85,6 +107,7 @@ class HeatMapInner extends Component {
<button <button
className='heatmap-nav-btn' className='heatmap-nav-btn'
disabled={!pages[this.state.pageIndex - 1]} disabled={!pages[this.state.pageIndex - 1]}
// eslint-disable-next-line @typescript-eslint/unbound-method
onClick={this.prevPage} onClick={this.prevPage}
style={{ style={{
visibility: pages[this.state.pageIndex - 1] ? 'unset' : 'hidden' visibility: pages[this.state.pageIndex - 1] ? 'unset' : 'hidden'
@ -96,6 +119,7 @@ class HeatMapInner extends Component {
<button <button
className='heatmap-nav-btn' className='heatmap-nav-btn'
disabled={!pages[this.state.pageIndex + 1]} disabled={!pages[this.state.pageIndex + 1]}
// eslint-disable-next-line @typescript-eslint/unbound-method
onClick={this.nextPage} onClick={this.nextPage}
style={{ style={{
visibility: pages[this.state.pageIndex + 1] ? 'unset' : 'hidden' visibility: pages[this.state.pageIndex + 1] ? 'unset' : 'hidden'
@ -107,17 +131,22 @@ class HeatMapInner extends Component {
<Spacer /> <Spacer />
<CalendarHeatMap <CalendarHeatMap
classForValue={value => { // 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'; 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'; 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'; 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'; if (value.count >= 8) return 'color-scale-a-lot';
return 'color-empty'; return 'color-empty';
}} }}
endDate={endOfCalendar} endDate={endOfCalendar}
startDate={startOfCalendar} startDate={startOfCalendar}
tooltipDataAttrs={value => { tooltipDataAttrs={(value: { count: number; date: Date }) => {
const dateFormatted = const dateFormatted: string =
value && value.date value && value.date
? value.date.toLocaleDateString([localeCode, 'en-US'], { ? value.date.toLocaleDateString([localeCode, 'en-US'], {
year: 'numeric', year: 'numeric',
@ -156,10 +185,9 @@ class HeatMapInner extends Component {
} }
} }
HeatMapInner.propTypes = innerPropTypes; const HeatMap = (props: IHeatMapProps): JSX.Element => {
const HeatMap = props => {
const { t } = useTranslation(); const { t } = useTranslation();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { calendar } = props; const { calendar } = props;
/** /**
@ -168,13 +196,14 @@ const HeatMap = props => {
*/ */
// create array of timestamps and turn into milliseconds // 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])); const startOfTimestamps = startOfDay(new Date(timestamps[0]));
let endOfCalendar = startOfDay(Date.now()); let endOfCalendar = startOfDay(Date.now());
let startOfCalendar; let startOfCalendar;
// creates pages for heatmap // creates pages for heatmap
let pages = []; const pages: IPageData[] = [];
do { do {
startOfCalendar = addDays(addMonths(endOfCalendar, -6), 1); startOfCalendar = addDays(addMonths(endOfCalendar, -6), 1);
@ -191,7 +220,7 @@ const HeatMap = props => {
pages.reverse(); pages.reverse();
let calendarData = []; const calendarData: ICalendarData[] = [];
let dayCounter = pages[0].startOfCalendar; let dayCounter = pages[0].startOfCalendar;
// create an object for each day of the calendar period // create an object for each day of the calendar period
@ -258,6 +287,5 @@ const HeatMap = props => {
}; };
HeatMap.displayName = 'HeatMap'; HeatMap.displayName = 'HeatMap';
HeatMap.propTypes = propTypes;
export default HeatMap; export default HeatMap;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Media } from '@freecodecamp/react-bootstrap'; import { Media } from '@freecodecamp/react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -7,19 +6,19 @@ import { FullWidthRow } from '../../helpers';
import './portfolio.css'; import './portfolio.css';
const propTypes = { interface IPortfolioData {
portfolio: PropTypes.arrayOf( description: string;
PropTypes.shape({ id: string;
description: PropTypes.string, image: string;
id: PropTypes.string, title: string;
image: PropTypes.string, url: string;
title: PropTypes.string, }
url: PropTypes.string
})
)
};
function Portfolio({ portfolio = [] }) { interface IPortfolioProps {
portfolio: IPortfolioData[];
}
function Portfolio({ portfolio = [] }: IPortfolioProps): JSX.Element | null {
const { t } = useTranslation(); const { t } = useTranslation();
if (!portfolio.length) { if (!portfolio.length) {
return null; return null;
@ -54,6 +53,5 @@ function Portfolio({ portfolio = [] }) {
} }
Portfolio.displayName = 'Portfolio'; Portfolio.displayName = 'Portfolio';
Portfolio.propTypes = propTypes;
export default Portfolio; export default Portfolio;

View File

@ -11,21 +11,21 @@ import { faLink } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import './social-icons.css'; import './social-icons.css';
const propTypes = { interface ISocialIconsProps {
email: PropTypes.string, email?: string;
githubProfile: PropTypes.string, githubProfile: string;
isGithub: PropTypes.bool, isGithub: boolean;
isLinkedIn: PropTypes.bool, isLinkedIn: boolean;
isTwitter: PropTypes.bool, isTwitter: boolean;
isWebsite: PropTypes.bool, isWebsite: boolean;
linkedin: PropTypes.string, linkedin: string;
show: PropTypes.bool, show?: boolean;
twitter: PropTypes.string, twitter: string;
username: PropTypes.string, username: string;
website: PropTypes.string website: string;
}; }
function LinkedInIcon(linkedIn, username) { function LinkedInIcon(linkedIn: string, username: string): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<a <a
@ -39,7 +39,7 @@ function LinkedInIcon(linkedIn, username) {
); );
} }
function GithubIcon(ghURL, username) { function GithubIcon(ghURL: string, username: string): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<a <a
@ -53,7 +53,7 @@ function GithubIcon(ghURL, username) {
); );
} }
function WebsiteIcon(website, username) { function WebsiteIcon(website: string, username: string): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<a <a
@ -67,7 +67,7 @@ function WebsiteIcon(website, username) {
); );
} }
function TwitterIcon(handle, username) { function TwitterIcon(handle: string, username: string): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<a <a
@ -81,7 +81,7 @@ function TwitterIcon(handle, username) {
); );
} }
function SocialIcons(props) { function SocialIcons(props: ISocialIconsProps): JSX.Element | null {
const { const {
githubProfile, githubProfile,
isLinkedIn, isLinkedIn,
@ -111,6 +111,5 @@ function SocialIcons(props) {
} }
SocialIcons.displayName = 'SocialIcons'; SocialIcons.displayName = 'SocialIcons';
SocialIcons.propTypes = propTypes;
export default SocialIcons; export default SocialIcons;

View File

@ -4,6 +4,7 @@ import TimeLine from './TimeLine';
import { useStaticQuery } from 'gatsby'; import { useStaticQuery } from 'gatsby';
beforeEach(() => { beforeEach(() => {
// @ts-ignore
useStaticQuery.mockImplementationOnce(() => ({ useStaticQuery.mockImplementationOnce(() => ({
allChallengeNode: { allChallengeNode: {
edges: [ edges: [
@ -41,6 +42,7 @@ beforeEach(() => {
describe('<TimeLine />', () => { describe('<TimeLine />', () => {
it('Render button when only solution is present', () => { it('Render button when only solution is present', () => {
// @ts-ignore
const { container } = render(<TimeLine {...propsForOnlySolution} />); const { container } = render(<TimeLine {...propsForOnlySolution} />);
expect( expect(
@ -49,11 +51,13 @@ describe('<TimeLine />', () => {
}); });
it('Render button when both githubLink and solution is present', () => { it('Render button when both githubLink and solution is present', () => {
// @ts-ignore
const { container } = render(<TimeLine {...propsForOnlySolution} />); const { container } = render(<TimeLine {...propsForOnlySolution} />);
const linkList = container.querySelector( const linkList = container.querySelector(
'#dropdown-for-5e4f5c4b570f7e3a4949899f + ul' '#dropdown-for-5e4f5c4b570f7e3a4949899f + ul'
); );
// @ts-ignore
const links = linkList.querySelectorAll('a'); const links = linkList.querySelectorAll('a');
expect(links[0]).toHaveAttribute( expect(links[0]).toHaveAttribute(
@ -68,6 +72,7 @@ describe('<TimeLine />', () => {
}); });
it('rendering the correct button when files is present', () => { it('rendering the correct button when files is present', () => {
// @ts-ignore
const { getByText } = render(<TimeLine {...propsForOnlySolution} />); const { getByText } = render(<TimeLine {...propsForOnlySolution} />);
const button = getByText('buttons.show-code'); const button = getByText('buttons.show-code');

View File

@ -1,5 +1,4 @@
import React, { Component, useMemo } from 'react'; import React, { Component, useMemo } from 'react';
import PropTypes from 'prop-types';
import { reverse, sortBy } from 'lodash-es'; import { reverse, sortBy } from 'lodash-es';
import { import {
Button, Button,
@ -9,7 +8,7 @@ import {
MenuItem MenuItem
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import { useStaticQuery, graphql } from 'gatsby'; import { useStaticQuery, graphql } from 'gatsby';
import { withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import './timeline.css'; import './timeline.css';
import TimelinePagination from './TimelinePagination'; import TimelinePagination from './TimelinePagination';
@ -26,63 +25,75 @@ import { maybeUrlRE } from '../../../utils';
import CertificationIcon from '../../../assets/icons/certification-icon'; import CertificationIcon from '../../../assets/icons/certification-icon';
import { langCodes } from '../../../../../config/i18n/all-langs'; import { langCodes } from '../../../../../config/i18n/all-langs';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import envData from '../../../../../config/env.json'; import envData from '../../../../../config/env.json';
const SolutionViewer = Loadable(() => const SolutionViewer = Loadable(
import('../../SolutionViewer/SolutionViewer') () =>
// 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; 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]; const localeCode = langCodes[clientLocale];
// Items per page in timeline. // Items per page in timeline.
const ITEMS_PER_PAGE = 15; const ITEMS_PER_PAGE = 15;
const propTypes = { interface ICompletedMap {
completedMap: PropTypes.arrayOf( id: string;
PropTypes.shape({ completedDate: number;
id: PropTypes.string, challengeType: number;
completedDate: PropTypes.number, solution: string;
challengeType: PropTypes.number, files: IFile[];
solution: PropTypes.string, githubLink: string;
files: PropTypes.arrayOf( }
PropTypes.shape({
ext: PropTypes.string,
contents: PropTypes.string
})
)
})
),
t: PropTypes.func.isRequired,
username: PropTypes.string
};
const innerPropTypes = { interface ITimelineProps {
...propTypes, completedMap: ICompletedMap[];
idToNameMap: PropTypes.objectOf( t: TFunction<'translation'>;
PropTypes.shape({ username: string;
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
};
class TimelineInner extends Component { interface IFile {
constructor(props) { ext: string;
contents: string;
}
interface ISortedTimeline {
id: string;
completedDate: number;
files: IFile[];
githubLink: string;
solution: string;
}
interface ITimelineInnerProps extends ITimelineProps {
idToNameMap: Map<string, string>;
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); super(props);
this.state = { this.state = {
@ -103,7 +114,12 @@ class TimelineInner extends Component {
this.renderViewButton = this.renderViewButton.bind(this); 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; const { t } = this.props;
if (files && files.length) { if (files && files.length) {
return ( return (
@ -165,10 +181,12 @@ class TimelineInner extends Component {
} }
} }
renderCompletion(completed) { renderCompletion(completed: ISortedTimeline): JSX.Element {
const { idToNameMap, username } = this.props; const { idToNameMap, username } = this.props;
const { id, files, githubLink, solution } = completed; const { id, files, githubLink, solution } = completed;
const completedDate = new Date(completed.completedDate); const completedDate = new Date(completed.completedDate);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id); const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
return ( return (
<tr className='timeline-row' key={id}> <tr className='timeline-row' key={id}>
@ -199,7 +217,7 @@ class TimelineInner extends Component {
</tr> </tr>
); );
} }
viewSolution(id, solution, files) { viewSolution(id: string, solution: string, files: IFile[]): void {
this.setState(state => ({ this.setState(state => ({
...state, ...state,
solutionToView: id, solutionToView: id,
@ -286,13 +304,17 @@ class TimelineInner extends Component {
<Modal.Header closeButton={true}> <Modal.Header closeButton={true}>
<Modal.Title id='contained-modal-title'> <Modal.Title id='contained-modal-title'>
{`${username}'s Solution to ${ {`${username}'s Solution to ${
// @ts-ignore
idToNameMap.get(id).challengeTitle idToNameMap.get(id).challengeTitle
}`} }`}
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{/* @ts-ignore */}
<SolutionViewer <SolutionViewer
// @ts-ignore
files={this.state.files} files={this.state.files}
// @ts-ignore
solution={this.state.solution} solution={this.state.solution}
/> />
</Modal.Body> </Modal.Body>
@ -316,9 +338,7 @@ class TimelineInner extends Component {
} }
} }
TimelineInner.propTypes = innerPropTypes; function useIdToNameMap(): Map<string, string> {
function useIdToNameMap() {
const { const {
allChallengeNode: { edges } allChallengeNode: { edges }
} = useStaticQuery(graphql` } = useStaticQuery(graphql`
@ -337,7 +357,7 @@ function useIdToNameMap() {
} }
`); `);
const idToNameMap = new Map(); const idToNameMap = new Map();
for (let id of getCertIds()) { for (const id of getCertIds()) {
idToNameMap.set(id, { idToNameMap.set(id, {
challengeTitle: `${getTitleFromId(id)} Certification`, challengeTitle: `${getTitleFromId(id)} Certification`,
certPath: getPathFromID(id) certPath: getPathFromID(id)
@ -346,8 +366,11 @@ function useIdToNameMap() {
edges.forEach( edges.forEach(
({ ({
node: { node: {
// @ts-ignore
id, id,
// @ts-ignore
title, title,
// @ts-ignore
fields: { slug } fields: { slug }
} }
}) => { }) => {
@ -357,7 +380,7 @@ function useIdToNameMap() {
return idToNameMap; return idToNameMap;
} }
const Timeline = props => { const Timeline = (props: ITimelineProps): JSX.Element => {
const idToNameMap = useIdToNameMap(); const idToNameMap = useIdToNameMap();
const { completedMap } = props; const { completedMap } = props;
// Get the sorted timeline along with total page count. // Get the sorted timeline along with total page count.
@ -380,8 +403,6 @@ const Timeline = props => {
); );
}; };
Timeline.propTypes = propTypes;
Timeline.displayName = 'Timeline'; Timeline.displayName = 'Timeline';
export default withTranslation()(Timeline); export default withTranslation()(Timeline);

View File

@ -3,7 +3,16 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; 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 { pageNo, totalPages, firstPage, prevPage, nextPage, lastPage } = props;
const { t } = useTranslation(); 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; export default TimelinePagination;

38
package-lock.json generated
View File

@ -7372,6 +7372,15 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true "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": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -7408,6 +7417,29 @@
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==", "integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==",
"dev": true "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": { "@types/sinonjs__fake-timers": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz", "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": { "currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",

View File

@ -142,4 +142,4 @@
"typescript": "4.3.4", "typescript": "4.3.4",
"webpack-bundle-analyzer": "4.4.2" "webpack-bundle-analyzer": "4.4.2"
} }
} }