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
30
client/package-lock.json
generated
30
client/package-lock.json
generated
@ -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",
|
||||
|
@ -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', () => {
|
@ -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;
|
@ -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;
|
@ -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);
|
@ -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
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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');
|
@ -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);
|
@ -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
38
package-lock.json
generated
@ -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",
|
||||
|
Reference in New Issue
Block a user