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:
committed by
Mrugesh Mohapatra
parent
f15a55e2b4
commit
da461bf09a
32
client/package-lock.json
generated
32
client/package-lock.json
generated
@ -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=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -160,4 +160,4 @@
|
|||||||
"webpack": "5.41.1",
|
"webpack": "5.41.1",
|
||||||
"webpack-cli": "4.7.2"
|
"webpack-cli": "4.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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', () => {
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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');
|
@ -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);
|
@ -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
38
package-lock.json
generated
@ -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",
|
||||||
|
@ -142,4 +142,4 @@
|
|||||||
"typescript": "4.3.4",
|
"typescript": "4.3.4",
|
||||||
"webpack-bundle-analyzer": "4.4.2"
|
"webpack-bundle-analyzer": "4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user