feat(client): ts-migration for Introduction templates (#43752)

* chore: rename the file CertificationCard.js to tsx

* refactor: refactor the file CertificationCard to tsx

refactor: refactor the file CertificationCard to tsx

refactor: refactor the file CertificationCard to tsx

* chore: rename the file ClaimCertSteps.js to tsx

* refactor: refactor the file ClaimCertSteps to TS

* chore: rename the file SuperBlockIntro.js to tsx

* refactor: refactor the file SuperBlockIntro to TS

* chore: rename the file Block.js to tsx

* refactor: refactor the file Block to TS

refactor: refactor the file Block to TS

refactor: refactor the file Block to TS

* chore: rename the file CertChallenge.js to tsx

* refactor: refactor the file CertChallenge to TS

* fix typing

* ignore missing redux store connection

Co-authored-by: ismail <i.tlemcani@quinten-maroc.com>
Co-authored-by: Ismail Tlemcani <ismail.tlemcani@gmail.com>
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Ismail Tlemcani
2021-11-30 19:36:33 +01:00
committed by GitHub
parent 513f27e408
commit 845659105f
8 changed files with 164 additions and 124 deletions

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import { HandlerProps } from 'react-reflex';
import { SuperBlocks } from '../../../config/certification-settings';
import { certMap } from '../resources/cert-and-project-map';
export const UserPropType = PropTypes.shape({
about: PropTypes.string,
@ -63,12 +64,13 @@ export const CurrentCertsPropType = PropTypes.arrayOf(
})
);
export const StepsPropType = PropTypes.shape({
currentCerts: CurrentCertsPropType,
isShowCerts: PropTypes.bool,
isShowName: PropTypes.bool,
isShowProfile: PropTypes.bool
});
export type Steps = {
isHonest?: boolean;
currentCerts?: Array<CurrentCert>;
isShowCerts?: boolean;
isShowName?: boolean;
isShowProfile?: boolean;
};
export type CurrentCert = {
show: boolean;
@ -83,7 +85,7 @@ export type MarkdownRemark = {
block: string;
isBeta: boolean;
superBlock: SuperBlocks;
title: string;
title: typeof certMap[number]['title'];
};
headings: [
{

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import { withTranslation, TFunction } from 'react-i18next';
import { connect } from 'react-redux';
import ScrollableAnchor from 'react-scrollable-anchor';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createSelector } from 'reselect';
import store from 'store';
@ -12,60 +11,69 @@ import { isAuditedCert } from '../../../../../utils/is-audited';
import Caret from '../../../assets/icons/caret';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { Link } from '../../../components/helpers/';
import { Link } from '../../../components/helpers';
import { completedChallengesSelector, executeGA } from '../../../redux';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import Challenges from './Challenges';
const { curriculumLocale } = envData;
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = (
state: unknown,
ownProps: { blockDashedName: string } & unknown
) => {
const expandedSelector = makeExpandedBlockSelector(ownProps.blockDashedName);
return createSelector(
expandedSelector,
completedChallengesSelector,
(isExpanded, completedChallenges) => ({
(isExpanded: boolean, completedChallenges: CompletedChallenge[]) => ({
isExpanded,
completedChallenges: completedChallenges.map(({ id }) => id)
completedChallengeIds: completedChallenges.map(({ id }) => id)
})
)(state);
};
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ toggleBlock, executeGA }, dispatch);
const propTypes = {
blockDashedName: PropTypes.string,
challenges: PropTypes.array,
completedChallenges: PropTypes.arrayOf(PropTypes.string),
executeGA: PropTypes.func,
isExpanded: PropTypes.bool,
superBlock: PropTypes.string,
t: PropTypes.func,
toggleBlock: PropTypes.func.isRequired
};
interface BlockProps {
blockDashedName: string;
challenges: ChallengeNode[];
completedChallengeIds: string[];
executeGA: typeof executeGA;
isExpanded: boolean;
superBlock: string;
t: TFunction;
toggleBlock: typeof toggleBlock;
}
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
export class Block extends Component {
constructor(...props) {
super(...props);
export class Block extends Component<BlockProps> {
static displayName: string;
constructor(props: BlockProps) {
super(props);
this.handleBlockClick = this.handleBlockClick.bind(this);
}
handleBlockClick() {
handleBlockClick(): void {
const { blockDashedName, toggleBlock, executeGA } = this.props;
const playSound = store.get('fcc-sound');
const playSound = store.get('fcc-sound') as boolean;
if (playSound) {
void import('tone').then(tone => {
void (async () => {
const tone = await import('tone');
const player = new tone.Player(
'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3'
).toDestination();
if (tone.context.state !== 'running') tone.context.resume();
if (tone.context.state !== 'running') {
void tone.context.resume();
}
player.autostart = playSound;
});
})();
}
executeGA({
type: 'event',
@ -74,10 +82,10 @@ export class Block extends Component {
action: blockDashedName
}
});
return toggleBlock(blockDashedName);
toggleBlock(blockDashedName);
}
renderCheckMark(isCompleted) {
renderCheckMark(isCompleted: boolean): JSX.Element {
return isCompleted ? (
<GreenPass style={mapIconStyle} />
) : (
@ -85,7 +93,7 @@ export class Block extends Component {
);
}
renderBlockIntros(arr) {
renderBlockIntros(arr: string[]): JSX.Element {
return (
<div className='block-description'>
{arr.map((str, i) => (
@ -95,10 +103,10 @@ export class Block extends Component {
);
}
render() {
render(): JSX.Element {
const {
blockDashedName,
completedChallenges,
completedChallengeIds,
challenges,
isExpanded,
superBlock,
@ -108,8 +116,8 @@ export class Block extends Component {
let completedCount = 0;
const challengesWithCompleted = challenges.map(challenge => {
const { id } = challenge;
const isCompleted = completedChallenges.some(
completedId => id === completedId
const isCompleted = completedChallengeIds.some(
(completedChallengeId: string) => completedChallengeId === id
);
if (isCompleted) {
completedCount++;
@ -134,13 +142,19 @@ export class Block extends Component {
);
});
const blockIntroObj = t(`intro:${superBlock}.blocks.${blockDashedName}`);
const blockIntroObj: { title?: string; intro: string[] } = t(
`intro:${superBlock}.blocks.${blockDashedName}`
);
const blockTitle = blockIntroObj ? blockIntroObj.title : null;
const blockIntroArr = blockIntroObj ? blockIntroObj.intro : [];
const {
expand: expandText,
collapse: collapseText,
courses: coursesText
}: {
expand: string;
collapse: string;
courses: string;
} = t('intro:misc-text');
return isProjectBlock ? (
@ -196,7 +210,9 @@ export class Block extends Component {
<button
aria-expanded={isExpanded}
className='map-title'
onClick={this.handleBlockClick}
onClick={() => {
this.handleBlockClick();
}}
>
<Caret />
<h4 className='course-title'>
@ -224,7 +240,6 @@ export class Block extends Component {
}
Block.displayName = 'Block';
Block.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,13 +1,13 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { navigate } from 'gatsby-link';
import PropTypes from 'prop-types';
import React, { useState, useEffect } from 'react';
import { withTranslation } from 'react-i18next';
import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
certSlugTypeMap,
superBlockCertTypeMap
superBlockCertTypeMap,
SuperBlocks
} from '../../../../../config/certification-settings';
import { createFlashMessage } from '../../../components/Flash/redux';
import {
@ -15,41 +15,40 @@ import {
stepsToClaimSelector,
isSignedInSelector
} from '../../../redux';
import { StepsPropType, UserPropType } from '../../../redux/prop-types';
import { User, Steps } from '../../../redux/prop-types';
import { verifyCert } from '../../../redux/settings';
import { certMap } from '../../../resources/cert-and-project-map';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import CertificationCard from './CertificationCard';
import CertificationCard from './certification-card';
const propTypes = {
createFlashMessage: PropTypes.func.isRequired,
fetchState: PropTypes.shape({
pending: PropTypes.bool,
complete: PropTypes.bool,
errored: PropTypes.bool
}),
isSignedIn: PropTypes.bool,
steps: StepsPropType,
superBlock: PropTypes.string,
t: PropTypes.func,
title: PropTypes.string,
user: UserPropType,
verifyCert: PropTypes.func.isRequired
interface CertChallengeProps {
createFlashMessage: typeof createFlashMessage;
fetchState: {
pending: boolean;
complete: boolean;
errored: boolean;
error: null | string;
};
isSignedIn: boolean;
steps: Steps;
superBlock: SuperBlocks;
t: TFunction;
title: typeof certMap[number]['title'];
user: User;
verifyCert: typeof verifyCert;
}
const honestyInfoMessage = {
type: 'info',
message: 'flash.honest-first'
};
const mapStateToProps = state => {
const mapStateToProps = (state: unknown) => {
return createSelector(
stepsToClaimSelector,
userFetchStateSelector,
isSignedInSelector,
(steps, fetchState, isSignedIn) => ({
(steps, fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
steps,
fetchState,
isSignedIn
@ -72,7 +71,7 @@ const CertChallenge = ({
fetchState,
isSignedIn,
user: { isHonest, username }
}) => {
}: CertChallengeProps): JSX.Element => {
const [canClaimCert, setCanClaimCert] = useState(false);
const [certVerificationMessage, setCertVerificationMessage] = useState('');
const [isCertified, setIsCertified] = useState(false);
@ -88,7 +87,7 @@ const CertChallenge = ({
useEffect(() => {
if (username) {
(async () => {
void (async () => {
try {
const data = await getVerifyCanClaimCert(username, superBlock);
const { status, result } = data?.response?.message;
@ -103,7 +102,8 @@ const CertChallenge = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username]);
const { certSlug } = certMap.find(x => x.title === title);
// @ts-expect-error Typescript is confused
const certSlug = certMap.find(x => x.title === title).certSlug;
useEffect(() => {
const { pending, complete } = fetchState;
@ -113,20 +113,26 @@ const CertChallenge = ({
}
}, [fetchState]);
const certSlugTypeMapTyped: { [key: string]: string } = certSlugTypeMap;
const superBlockCertTypeMapTyped: { [key: string]: string } =
superBlockCertTypeMap;
useEffect(() => {
setIsCertified(
steps?.currentCerts?.find(
cert =>
certSlugTypeMap[cert.certSlug] === superBlockCertTypeMap[superBlock]
(cert: { certSlug: string }) =>
certSlugTypeMapTyped[cert.certSlug] ===
superBlockCertTypeMapTyped[superBlock]
)?.show ?? false
);
const projectsCompleted =
canClaimCert || certVerificationMessage === 'projects-completed';
const projectsCompletedNumber = projectsCompleted ? 1 : 0;
const completedCount =
Object.values(steps).filter(
stepVal => typeof stepVal === 'boolean' && stepVal
).length + projectsCompleted;
).length + projectsCompletedNumber;
const numberOfSteps = Object.keys(steps).length;
setHasCompletedRequiredSteps(completedCount === numberOfSteps);
setStepState({ numberOfSteps, completedCount });
@ -145,7 +151,8 @@ const CertChallenge = ({
isSignedIn &&
(!isCertified || (!hasCompletedRequiredSteps && verificationComplete));
const createClickHandler = certSlug => e => {
const createClickHandler =
(certSlug: string | undefined) => (e: { preventDefault: () => void }) => {
e.preventDefault();
if (isCertified) {
return navigate(certLocation);
@ -188,7 +195,6 @@ const CertChallenge = ({
};
CertChallenge.displayName = 'CertChallenge';
CertChallenge.propTypes = propTypes;
export { CertChallenge };

View File

@ -1,26 +1,24 @@
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ScrollableAnchor from 'react-scrollable-anchor';
import { SuperBlocks } from '../../../../../config/certification-settings';
import Caret from '../../../assets/icons/caret';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
// import { navigate } from 'gatsby';
import GreenPass from '../../../assets/icons/green-pass';
import { StepsPropType } from '../../../redux/prop-types';
import ClaimCertSteps from './ClaimCertSteps';
import { Steps } from '../../../redux/prop-types';
import ClaimCertSteps from './claim-cert-steps';
const propTypes = {
i18nCertText: PropTypes.string,
isProjectsCompleted: PropTypes.bool,
stepState: PropTypes.shape({
numberOfSteps: PropTypes.number,
completedCount: PropTypes.number
}),
steps: StepsPropType,
superBlock: PropTypes.string
interface CertificationCardProps {
i18nCertText: string;
isProjectsCompleted: boolean;
stepState: {
numberOfSteps: number;
completedCount: number;
};
steps: Steps;
superBlock: SuperBlocks;
}
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
const CertificationCard = ({
@ -29,7 +27,7 @@ const CertificationCard = ({
i18nCertText,
stepState: { completedCount, numberOfSteps },
steps
}) => {
}: CertificationCardProps): JSX.Element => {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(true);
@ -41,6 +39,10 @@ const CertificationCard = ({
expand: expandText,
collapse: collapseText,
courses: coursesText
}: {
expand: string;
collapse: string;
courses: string;
} = t('intro:misc-text');
return (
<ScrollableAnchor id='claim-cert-block'>
@ -90,6 +92,5 @@ const CertificationCard = ({
};
CertificationCard.displayName = 'CertStatus';
CertificationCard.propTypes = propTypes;
export default CertificationCard;

View File

@ -1,28 +1,28 @@
import { Link } from 'gatsby';
import PropTypes from 'prop-types';
import React from 'react';
import { withTranslation, useTranslation } from 'react-i18next';
import { SuperBlocks } from '../../../../../config/certification-settings';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { StepsPropType } from '../../../redux/prop-types';
import { Steps } from '../../../redux/prop-types';
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
const propTypes = {
i18nCertText: PropTypes.string,
isProjectsCompleted: PropTypes.bool,
steps: StepsPropType,
superBlock: PropTypes.string
};
interface ClaimCertStepsProps {
i18nCertText: string;
isProjectsCompleted: boolean;
steps: Steps;
superBlock: SuperBlocks;
}
const ClaimCertSteps = ({
isProjectsCompleted,
i18nCertText,
steps,
superBlock
}) => {
}: ClaimCertStepsProps): JSX.Element => {
const { t } = useTranslation();
const renderCheckMark = isCompleted => {
const renderCheckMark = (isCompleted: boolean) => {
return isCompleted ? (
<GreenPass style={mapIconStyle} />
) : (
@ -84,6 +84,5 @@ const ClaimCertSteps = ({
};
ClaimCertSteps.displayName = 'ClaimCertSteps';
ClaimCertSteps.propTypes = propTypes;
export default withTranslation()(ClaimCertSteps);

View File

@ -1,19 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SuperBlocks } from '../../../../../config/certification-settings';
import { generateIconComponent } from '../../../assets/icons';
import { Spacer } from '../../../components/helpers';
const propTypes = {
superBlock: PropTypes.string
};
interface SuperBlockIntroProps {
superBlock: SuperBlocks;
}
function SuperBlockIntro(props) {
function SuperBlockIntro(props: SuperBlockIntroProps): JSX.Element {
const { t } = useTranslation();
const { superBlock } = props;
const superBlockIntroObj = t(`intro:${superBlock}`);
const superBlockIntroObj: {
title: string;
intro: string[];
note: string[];
} = t(`intro:${superBlock}`);
const {
title: i18nSuperBlock,
intro: superBlockIntroText,
@ -39,6 +42,5 @@ function SuperBlockIntro(props) {
}
SuperBlockIntro.displayName = 'SuperBlockIntro';
SuperBlockIntro.propTypes = propTypes;
export default SuperBlockIntro;

View File

@ -25,9 +25,9 @@ import {
userSelector
} from '../../redux';
import { MarkdownRemark, AllChallengeNode, User } from '../../redux/prop-types';
import Block from './components/Block';
import CertChallenge from './components/CertChallenge';
import SuperBlockIntro from './components/SuperBlockIntro';
import Block from './components/block';
import CertChallenge from './components/cert-challenge';
import SuperBlockIntro from './components/super-block-intro';
import { resetExpansion, toggleBlock } from './redux';
import './intro.css';

View File

@ -3,6 +3,7 @@ import envData from '../../../config/env.json';
import type {
ChallengeFile,
ClaimedCertifications,
CompletedChallenge,
User
} from '../redux/prop-types';
@ -158,11 +159,25 @@ export function getUsernameExists(username: string): Promise<boolean> {
return get(`/api/users/exists?username=${username}`);
}
// TODO: Does a GET return a bolean?
export interface GetVerifyCanClaimCert {
response: {
type: string;
message: {
status: boolean;
result: string;
};
variables: {
name: string;
};
};
isCertMap: ClaimedCertifications;
completedChallenges: CompletedChallenge[];
}
export function getVerifyCanClaimCert(
username: string,
superBlock: string
): Promise<boolean> {
): Promise<GetVerifyCanClaimCert> {
return get(
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${superBlock}`
);