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-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",

View File

@ -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('<Profile/>', () => {
it('renders the report button on another persons profile', () => {
const { getByText } = render(<Profile {...notMyProfileProps} />);
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', () => {

View File

@ -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 ? (
<Fragment>
<FullWidthRow>
@ -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 (
<Fragment>
<Camper
about={showAbout ? about : null}
about={about}
githubProfile={githubProfile}
isDonating={showDonation ? isDonating : null}
isDonating={showDonation ? isDonating : false}
isGithub={isGithub}
isLinkedIn={isLinkedIn}
isTwitter={isTwitter}
isWebsite={isWebsite}
joinDate={showAbout ? joinDate : null}
joinDate={showAbout ? joinDate : ''}
linkedin={linkedin}
location={showLocation ? location : null}
name={showName ? name : null}
location={showLocation ? location : ''}
name={showName ? name : ''}
picture={picture}
points={showPoints ? points : null}
twitter={twitter}
@ -138,6 +144,7 @@ function renderProfile(user) {
website={website}
yearsTopContributor={yearsTopContributor}
/>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
{showHeatMap ? <HeatMap calendar={calendar} /> : null}
{showCerts ? <Certifications username={username} /> : 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 {
profileUI: { isLocked = true },
@ -180,6 +187,5 @@ function Profile({ user, isSessionUser }) {
}
Profile.displayName = 'Profile';
Profile.propTypes = propTypes;
export default Profile;

View File

@ -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;

View File

@ -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 ? (
<Fragment key={cert.title}>
<Row>
@ -55,14 +71,14 @@ function Certificates({
hasLegacyCert,
hasModernCert,
username
}) {
}: ICertificationProps): JSX.Element {
const { t } = useTranslation();
const renderCertShowWithUsername = curry(renderCertShow)(username);
return (
<FullWidthRow className='certifications'>
<h2 className='text-center'>{t('profile.fcc-certs')}</h2>
<br />
{hasModernCert ? (
{hasModernCert && currentCerts ? (
currentCerts.map(renderCertShowWithUsername)
) : (
<p className='text-center'>{t('profile.no-certs')}</p>
@ -72,7 +88,7 @@ function Certificates({
<br />
<h3 className='text-center'>{t('settings.headings.legacy-certs')}</h3>
<br />
{legacyCerts.map(renderCertShowWithUsername)}
{legacyCerts && legacyCerts.map(renderCertShowWithUsername)}
<Spacer size={2} />
</div>
) : null}
@ -81,7 +97,6 @@ function Certificates({
);
}
Certificates.propTypes = propTypes;
Certificates.displayName = 'Certifications';
export default connect(mapStateToProps)(Certificates);

View File

@ -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<any, any>;
beforeEach(() => {
dateNowMockFn = jest

View File

@ -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<IHeatMapInnerProps, IHeatMapInnerState> {
constructor(props: IHeatMapInnerProps) {
super(props);
this.state = {
@ -85,6 +107,7 @@ class HeatMapInner extends Component {
<button
className='heatmap-nav-btn'
disabled={!pages[this.state.pageIndex - 1]}
// eslint-disable-next-line @typescript-eslint/unbound-method
onClick={this.prevPage}
style={{
visibility: pages[this.state.pageIndex - 1] ? 'unset' : 'hidden'
@ -96,6 +119,7 @@ class HeatMapInner extends Component {
<button
className='heatmap-nav-btn'
disabled={!pages[this.state.pageIndex + 1]}
// eslint-disable-next-line @typescript-eslint/unbound-method
onClick={this.nextPage}
style={{
visibility: pages[this.state.pageIndex + 1] ? 'unset' : 'hidden'
@ -107,17 +131,22 @@ class HeatMapInner extends Component {
<Spacer />
<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';
// 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;

View File

@ -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;

View File

@ -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 (
<a
@ -39,7 +39,7 @@ function LinkedInIcon(linkedIn, username) {
);
}
function GithubIcon(ghURL, username) {
function GithubIcon(ghURL: string, username: string): JSX.Element {
const { t } = useTranslation();
return (
<a
@ -53,7 +53,7 @@ function GithubIcon(ghURL, username) {
);
}
function WebsiteIcon(website, username) {
function WebsiteIcon(website: string, username: string): JSX.Element {
const { t } = useTranslation();
return (
<a
@ -67,7 +67,7 @@ function WebsiteIcon(website, username) {
);
}
function TwitterIcon(handle, username) {
function TwitterIcon(handle: string, username: string): JSX.Element {
const { t } = useTranslation();
return (
<a
@ -81,7 +81,7 @@ function TwitterIcon(handle, username) {
);
}
function SocialIcons(props) {
function SocialIcons(props: ISocialIconsProps): JSX.Element | null {
const {
githubProfile,
isLinkedIn,
@ -111,6 +111,5 @@ function SocialIcons(props) {
}
SocialIcons.displayName = 'SocialIcons';
SocialIcons.propTypes = propTypes;
export default SocialIcons;

View File

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

View File

@ -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(() =>
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<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);
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 (
<tr className='timeline-row' key={id}>
@ -199,7 +217,7 @@ class TimelineInner extends Component {
</tr>
);
}
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 {
<Modal.Header closeButton={true}>
<Modal.Title id='contained-modal-title'>
{`${username}'s Solution to ${
// @ts-ignore
idToNameMap.get(id).challengeTitle
}`}
</Modal.Title>
</Modal.Header>
<Modal.Body>
{/* @ts-ignore */}
<SolutionViewer
// @ts-ignore
files={this.state.files}
// @ts-ignore
solution={this.state.solution}
/>
</Modal.Body>
@ -316,9 +338,7 @@ class TimelineInner extends Component {
}
}
TimelineInner.propTypes = innerPropTypes;
function useIdToNameMap() {
function useIdToNameMap(): Map<string, string> {
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);

View File

@ -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;

38
package-lock.json generated
View File

@ -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",