From 3af73290bc6485c6ac6524912c26afd1bf5983a2 Mon Sep 17 00:00:00 2001 From: Shaun Hamilton Date: Fri, 28 Apr 2023 09:01:44 +0100 Subject: [PATCH] refactor(client): migrate certification to ts (#49708) * refactor(client): migrate certification to ts * fix: reduce certMap into projectMap etc. * fix: rename certMap -> fullCertMap * fix: cert-and-project-map strict typing * close but not close enough * fix: combine project and legacy project maps * chore: add type assertion safety comment * fix: add correct types to test mocks * fix: another test * remove settings-button.test.js The test did not handle any changes, and only served as a failing snapshot when projects are added/updated * refactor: use exported map types Co-authored by: Muhammed Mustafa * refactor: use exported map types Co-authored-by: Muhammed Mustafa --------- Co-authored-by: Muhammed Mustafa --- .../client-only-routes/show-certification.tsx | 4 +- .../components/ProgressBar/progress-bar.tsx | 8 +- .../settings-button.test.js.snap | 2491 ----------------- .../settings/certification.test.tsx | 145 +- .../{certification.js => certification.tsx} | 260 +- .../settings/settings-button.test.js | 48 - client/src/redux/prop-types.ts | 6 +- client/src/redux/settings/settings-sagas.js | 4 +- client/src/resources/cert-and-project-map.ts | 89 +- .../components/cert-challenge.tsx | 6 +- 10 files changed, 302 insertions(+), 2759 deletions(-) delete mode 100644 client/src/components/settings/__snapshots__/settings-button.test.js.snap rename client/src/components/settings/{certification.js => certification.tsx} (67%) delete mode 100644 client/src/components/settings/settings-button.test.js diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx index 0acc5406e33..035ec6257d8 100644 --- a/client/src/client-only-routes/show-certification.tsx +++ b/client/src/client-only-routes/show-certification.tsx @@ -26,7 +26,7 @@ import { usernameSelector } from '../redux/selectors'; import { UserFetchState, User } from '../redux/prop-types'; -import { certMap } from '../resources/cert-and-project-map'; +import { fullCertMap } from '../resources/cert-and-project-map'; import certificateMissingMessage from '../utils/certificate-missing-message'; import reallyWeirdErrorMessage from '../utils/really-weird-error-message'; import standardErrorMessage from '../utils/standard-error-message'; @@ -79,7 +79,7 @@ interface ShowCertificationProps { const requestedUserSelector = (state: unknown, { username = '' }) => userByNameSelector(username.toLowerCase())(state) as User; -const validCertSlugs = certMap.map(cert => cert.certSlug); +const validCertSlugs = fullCertMap.map(cert => cert.certSlug); const mapStateToProps = (state: unknown, props: ShowCertificationProps) => { const isValidCert = validCertSlugs.some(slug => slug === props.certSlug); diff --git a/client/src/components/ProgressBar/progress-bar.tsx b/client/src/components/ProgressBar/progress-bar.tsx index c1206bdfe96..7a02b866d4a 100644 --- a/client/src/components/ProgressBar/progress-bar.tsx +++ b/client/src/components/ProgressBar/progress-bar.tsx @@ -9,7 +9,7 @@ import { completedChallengesInBlockSelector, completedPercentageSelector } from '../../templates/Challenges/redux/selectors'; -import { certMap } from '../../resources/cert-and-project-map'; +import { certMapWithoutFullStack } from '../../resources/cert-and-project-map'; import ProgressBarInner from './progress-bar-inner'; const mapStateToProps = createSelector( @@ -55,10 +55,8 @@ function ProgressBar({ t }: ProgressBarProps): JSX.Element { const blockTitle = t(`intro:${superBlock}.blocks.${block}.title`); - const isCertificationProject = certMap.some(cert => { - if ('projects' in cert) { - return cert.projects.some((project: { id: string }) => project.id === id); - } + const isCertificationProject = certMapWithoutFullStack.some(cert => { + return cert.projects.some((project: { id: string }) => project.id === id); }); const totalChallengesInBlock = currentBlockIds?.length ?? 0; diff --git a/client/src/components/settings/__snapshots__/settings-button.test.js.snap b/client/src/components/settings/__snapshots__/settings-button.test.js.snap deleted file mode 100644 index a02c4b803fc..00000000000 --- a/client/src/components/settings/__snapshots__/settings-button.test.js.snap +++ /dev/null @@ -1,2491 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should check certification button consistency 1`] = ` -[ -
- , -
- , -
- , -
- , -
- , -
- , -
- , -
- , -
- , -
- , -
- , -
- , -] -`; - -exports[`should check legacy certification button consistency 1`] = ` -[ -
-
-
-

- certification.title.Legacy Front End -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- settings.labels.project-name - - settings.labels.solution -
- - certification.project.title.Build a Personal Portfolio Webpage - - -
- - certification.project.title.Build a Random Quote Machine - - -
- - certification.project.title.Build a 25 + 5 Clock - - -
- - certification.project.title.Build a JavaScript Calculator - - -
- - certification.project.title.Show the Local Weather - - -
- - certification.project.title.Use the TwitchTV JSON API - - -
- - certification.project.title.Build a Tribute Page - - -
- - certification.project.title.Build a Wikipedia Viewer - - -
- - certification.project.title.Build a Tic Tac Toe Game - - -
- - certification.project.title.Build a Simon Game - - -
- - buttons.claim-cert - - - Legacy Front End - - -
-
-
, -
-
-
-

- certification.title.Legacy Back End -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- settings.labels.project-name - - settings.labels.solution -
- - certification.project.title.Timestamp Microservice - - -
- - certification.project.title.Request Header Parser Microservice - - -
- - certification.project.title.URL Shortener Microservice - - -
- - certification.project.title.Image Search Abstraction Layer - - -
- - certification.project.title.File Metadata Microservice - - -
- - certification.project.title.Build a Voting App - - -
- - certification.project.title.Build a Nightlife Coordination App - - -
- - certification.project.title.Chart the Stock Market - - -
- - certification.project.title.Manage a Book Trading Club - - -
- - certification.project.title.Build a Pinterest Clone - - -
- - buttons.claim-cert - - - Legacy Back End - - -
-
-
, -
-
-
-

- certification.title.Legacy Data Visualization -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- settings.labels.project-name - - settings.labels.solution -
- - certification.project.title.Build a Markdown Previewer - - -
- - certification.project.title.Build a freeCodeCamp Forum Homepage - - -
- - certification.project.title.Build a Recipe Box - - -
- - certification.project.title.Build the Game of Life - - -
- - certification.project.title.Build a Roguelike Dungeon Crawler Game - - -
- - certification.project.title.Visualize Data with a Bar Chart - - -
- - certification.project.title.Visualize Data with a Scatterplot Graph - - -
- - certification.project.title.Visualize Data with a Heat Map - - -
- - certification.project.title.Show National Contiguity with a Force Directed Graph - - -
- - certification.project.title.Map Data Across the Globe - - -
- - buttons.claim-cert - - - Legacy Data Visualization - - -
-
-
, -
- , -] -`; diff --git a/client/src/components/settings/certification.test.tsx b/client/src/components/settings/certification.test.tsx index 4436277ae51..fd17ec8cc56 100644 --- a/client/src/components/settings/certification.test.tsx +++ b/client/src/components/settings/certification.test.tsx @@ -3,8 +3,11 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; import { createStore } from '../../redux/create-store'; +import { Ext } from '../../redux/prop-types'; +import { verifyCert } from '../../redux/settings/actions'; +import { createFlashMessage } from '../Flash/redux'; -import { CertificationSettings } from './certification'; +import CertificationSettings from './certification'; jest.mock('../../analytics'); @@ -106,127 +109,186 @@ const defaultTestProps = { completedChallenges: [ { id: 'bd7156d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7155d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7154d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7153d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7168d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7178d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7188d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7198d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7108d8c242eddfaeb5bd13', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443edefaeb5bdef', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443edefaeb5bdff', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443edefaeb5bd0e', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443edefaeb5bdee', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443edefaeb5bd0f', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443eddfaeb5bdef', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443eddfaeb5bdff', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443eddfaeb5bd0e', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443eddfaeb5bd0f', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7158d8c443eddfaeb5bdee', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e444147903586ffb414c94c', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e444147903586ffb414c94d', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e444147903586ffb414c94e', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e444147903586ffb414c94f', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e44414f903586ffb414c950', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e46f7e5ac417301a38fb928', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e46f7e5ac417301a38fb929', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e46f7f8ac417301a38fb92a', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e46f802ac417301a38fb92b', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e4f5c4b570f7e3a4949899f', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: 'bd7157d8c242eddfaeb5bd13', completedDate: 1554272923799, - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + challengeFiles: [] } ], - createFlashMessage: () => {}, + createFlashMessage: createFlashMessage, is2018DataVisCert: false, isApisMicroservicesCert: false, isBackEndCert: false, @@ -246,13 +308,14 @@ const defaultTestProps = { isRelationalDatabaseCertV8: false, isCollegeAlgebraPyCertV8: false, username: 'developmentuser', - verifyCert: () => {}, - errors: {}, - submit: () => {} + verifyCert: verifyCert, + isEmailVerified: false + // errors: {}, + // submit: () => {} }; const contents = 'This is not JS'; -const ext = 'js'; +const ext: Ext = 'js'; const fileKey = 'indexjs'; const name = 'index'; const path = 'index.js'; @@ -262,15 +325,20 @@ const propsForOnlySolution = { completedChallenges: [ { id: '5e46f802ac417301a38fb92b', - solution: 'https://github.com/freeCodeCamp/freeCodeCamp' + solution: 'https://github.com/freeCodeCamp/freeCodeCamp', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e4f5c4b570f7e3a4949899f', solution: 'https://github.com/freeCodeCamp/freeCodeCamp1', - githubLink: 'https://github.com/freeCodeCamp/freeCodeCamp2' + githubLink: 'https://github.com/freeCodeCamp/freeCodeCamp2', + completedDate: 123456789, + challengeFiles: [] }, { id: '5e46f7f8ac417301a38fb92a', + completedDate: 123456789, challengeFiles: [ { contents, @@ -289,6 +357,7 @@ const propsForMultifileProject = { completedChallenges: [ { id: '587d78af367417b2b2512b03', + completedDate: 123456789, challengeFiles: [ { contents, diff --git a/client/src/components/settings/certification.js b/client/src/components/settings/certification.tsx similarity index 67% rename from client/src/components/settings/certification.js rename to client/src/components/settings/certification.tsx index 89cf4b3ce9b..4e46bd3ec44 100644 --- a/client/src/components/settings/certification.js +++ b/client/src/components/settings/certification.tsx @@ -1,9 +1,9 @@ import { Table, Button } from '@freecodecamp/react-bootstrap'; import { Link, navigate } from 'gatsby'; -import { find, first } from 'lodash-es'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import { find } from 'lodash-es'; +import React, { MouseEvent, useState } from 'react'; import { withTranslation } from 'react-i18next'; +import type { TFunction } from 'i18next'; import { createSelector } from 'reselect'; import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor'; import { connect } from 'react-redux'; @@ -13,60 +13,41 @@ import ProjectPreviewModal from '../../templates/Challenges/components/project-p import { openModal } from '../../templates/Challenges/redux/actions'; import { projectMap, - legacyProjectMap + legacyProjectMap, + fullProjectMap, + ProjectMap, + LegacyProjectMap } from '../../resources/cert-and-project-map'; import { FlashMessages } from '../Flash/redux/flash-messages'; import ProjectModal from '../SolutionViewer/project-modal'; import { FullWidthRow, Spacer } from '../helpers'; import { SolutionDisplayWidget } from '../solution-display-widget'; -import SectionHeader from './section-header'; +import { certSlugTypeMap } from '../../../../config/certification-settings'; import './certification.css'; +import { + ClaimedCertifications, + CompletedChallenge, + User +} from '../../redux/prop-types'; +import { createFlashMessage } from '../Flash/redux'; +import { verifyCert } from '../../redux/settings/actions'; +import SectionHeader from './section-header'; configureAnchors({ offset: -40, scrollDuration: 0 }); -const propTypes = { - completedChallenges: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - solution: PropTypes.string, - githubLink: PropTypes.string, - challengeType: PropTypes.number, - completedDate: PropTypes.number, - challengeFiles: PropTypes.array - }) - ), - createFlashMessage: PropTypes.func.isRequired, - is2018DataVisCert: PropTypes.bool, - isApisMicroservicesCert: PropTypes.bool, - isBackEndCert: PropTypes.bool, - isDataAnalysisPyCertV7: PropTypes.bool, - isDataVisCert: PropTypes.bool, - isCollegeAlgebraPyCertV8: PropTypes.bool, - isFrontEndCert: PropTypes.bool, - isFrontEndLibsCert: PropTypes.bool, - isFullStackCert: PropTypes.bool, - isHonest: PropTypes.bool, - isInfosecCertV7: PropTypes.bool, - isInfosecQaCert: PropTypes.bool, - isJsAlgoDataStructCert: PropTypes.bool, - isMachineLearningPyCertV7: PropTypes.bool, - isQaCertV7: PropTypes.bool, - isRelationalDatabaseCertV8: PropTypes.bool, - isRespWebDesignCert: PropTypes.bool, - isSciCompPyCertV7: PropTypes.bool, - openModal: PropTypes.func, - t: PropTypes.func.isRequired, - username: PropTypes.string, - verifyCert: PropTypes.func.isRequired -}; - const mapDispatchToProps = { openModal }; -const certifications = Object.keys(projectMap); -const legacyCertifications = Object.keys(legacyProjectMap); +// Safety: projectMap definitely has projectMap keys, +// and we are only interested in these keys +const certifications = Object.keys(projectMap) as Array; +// Safety: legacyProjectMap definitely has legacyProjectMap keys, +// and we are only interested in these keys +const legacyCertifications = Object.keys(legacyProjectMap) as Array< + keyof LegacyProjectMap +>; const isCertSelector = ({ is2018DataVisCert, isApisMicroservicesCert, @@ -85,7 +66,7 @@ const isCertSelector = ({ isMachineLearningPyCertV7, isRelationalDatabaseCertV8, isCollegeAlgebraPyCertV8 -}) => ({ +}: ClaimedCertifications) => ({ is2018DataVisCert, isApisMicroservicesCert, isJsAlgoDataStructCert, @@ -149,33 +130,37 @@ const honestyInfoMessage = { message: FlashMessages.HonestFirst }; -const initialState = { - solutionViewer: { - projectTitle: '', - challengeFiles: null, - solution: null, - isOpen: false - } -}; +type CertificationSettingsProps = { + createFlashMessage: typeof createFlashMessage; + t: TFunction; + verifyCert: typeof verifyCert; + openModal: typeof openModal; +} & ClaimedCertifications & + Pick; -export class CertificationSettings extends Component { - constructor(props) { - super(props); - - this.state = { ...initialState }; +function CertificationSettings(props: CertificationSettingsProps) { + const [projectTitle, setProjectTitle] = useState(''); + const [challengeFiles, setChallengeFiles] = useState< + CompletedChallenge['challengeFiles'] | null + >(null); + const [challengeData, setChallengeData] = useState( + null + ); + const [solution, setSolution] = useState(); + const [isOpen, setIsOpen] = useState(false); + function initialiseState() { + setProjectTitle(''); + setChallengeFiles(null); + setSolution(null); + setIsOpen(false); } - createHandleLinkButtonClick = to => e => { - e.preventDefault(); - return navigate(to); - }; + const handleSolutionModalHide = () => initialiseState(); - handleSolutionModalHide = () => this.setState({ ...initialState }); + const getUserIsCertMap = () => isCertMapSelector(props); - getUserIsCertMap = () => isCertMapSelector(this.props); - - getProjectSolution = (projectId, projectTitle) => { - const { completedChallenges, openModal } = this.props; + const getProjectSolution = (projectId: string, projectTitle: string) => { + const { completedChallenges, openModal } = props; const completedProject = find( completedChallenges, ({ id }) => projectId === id @@ -185,16 +170,14 @@ export class CertificationSettings extends Component { } const { solution, challengeFiles } = completedProject; - const showUserCode = () => - this.setState({ - solutionViewer: { - projectTitle, - challengeFiles, - solution, - isOpen: true - } - }); + const showUserCode = () => { + setProjectTitle(projectTitle); + setChallengeFiles(challengeFiles); + setSolution(solution); + setIsOpen(true); + }; + // Type == ChallengeFile or CompletedChallenge? const challengeData = completedProject ? { ...completedProject, @@ -205,12 +188,8 @@ export class CertificationSettings extends Component { : null; const showProjectPreview = () => { - this.setState({ - projectViewer: { - previewTitle: projectTitle, - challengeData - } - }); + setProjectTitle(projectTitle); + setChallengeData(challengeData); openModal('projectPreview'); }; @@ -226,9 +205,10 @@ export class CertificationSettings extends Component { ); }; - renderCertifications = (certName, projectsMap) => { - const { t } = this.props; - const { certSlug } = first(projectsMap[certName]); + type CertName = keyof ProjectMap | keyof LegacyProjectMap; + function renderCertifications(certName: CertName) { + const { t } = props; + const { certSlug } = fullProjectMap[certName][0]; return ( @@ -243,22 +223,26 @@ export class CertificationSettings extends Component { - {this.renderProjectsFor( + {renderProjectsFor({ certName, - this.getUserIsCertMap()[certName], - projectsMap - )} + isCert: getUserIsCertMap()[certName] + })} ); - }; - renderProjectsFor = (certName, isCert, projectsMap) => { - const { username, isHonest, createFlashMessage, t, verifyCert } = - this.props; - const { certSlug } = first(projectsMap[certName]); + } + function renderProjectsFor({ + certName, + isCert + }: { + certName: CertName; + isCert: boolean; + }) { + const { username, isHonest, createFlashMessage, t, verifyCert } = props; + const { certSlug } = fullProjectMap[certName][0]; const certLocation = `/certification/${username}/${certSlug}`; - const createClickHandler = certSlug => e => { + const clickHandler = (e: MouseEvent) => { e.preventDefault(); if (isCert) { return navigate(certLocation); @@ -267,7 +251,7 @@ export class CertificationSettings extends Component { ? verifyCert(certSlug) : createFlashMessage(honestyInfoMessage); }; - return projectsMap[certName] + return fullProjectMap[certName] .map(({ link, title, id }) => ( @@ -276,7 +260,7 @@ export class CertificationSettings extends Component { - {this.getProjectSolution(id, title)} + {getProjectSolution(id, title)} )) @@ -289,7 +273,7 @@ export class CertificationSettings extends Component { className={'col-xs-12'} href={certLocation} data-cy={`btn-for-${certSlug}`} - onClick={createClickHandler(certSlug)} + onClick={clickHandler} > {isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}{' '} {certName} @@ -297,9 +281,9 @@ export class CertificationSettings extends Component { ]); - }; + } - renderLegacyFullStack = () => { + const renderLegacyFullStack = () => { const { isFullStackCert, username, @@ -313,7 +297,7 @@ export class CertificationSettings extends Component { isJsAlgoDataStructCert, isRespWebDesignCert, t - } = this.props; + } = props; const fullStackClaimable = is2018DataVisCert && @@ -334,15 +318,17 @@ export class CertificationSettings extends Component { fontSize: '18px' }; - const createClickHandler = certSlug => e => { - e.preventDefault(); - if (isFullStackCert) { - return navigate(certLocation); - } - return isHonest - ? verifyCert(certSlug) - : createFlashMessage(honestyInfoMessage); - }; + const createClickHandler = + (certSlug: keyof typeof certSlugTypeMap) => + (e: MouseEvent) => { + e.preventDefault(); + if (isFullStackCert) { + return navigate(certLocation); + } + return isHonest + ? verifyCert(certSlug) + : createFlashMessage(honestyInfoMessage); + }; return ( @@ -408,39 +394,37 @@ export class CertificationSettings extends Component { ); }; - render() { - const { solutionViewer, projectViewer } = this.state; - const { t } = this.props; + const { t } = props; - return ( - -
- {t('settings.headings.certs')} - {certifications.map(certName => - this.renderCertifications(certName, projectMap) - )} - {t('settings.headings.legacy-certs')} - {this.renderLegacyFullStack()} - {legacyCertifications.map(certName => - this.renderCertifications(certName, legacyProjectMap) - )} - - -
-
- ); - } + return ( + +
+ {t('settings.headings.certs')} + {certifications.map(certName => renderCertifications(certName))} + {t('settings.headings.legacy-certs')} + {renderLegacyFullStack()} + {legacyCertifications.map(certName => renderCertifications(certName))} + + +
+
+ ); } CertificationSettings.displayName = 'CertificationSettings'; -CertificationSettings.propTypes = propTypes; export default connect( null, diff --git a/client/src/components/settings/settings-button.test.js b/client/src/components/settings/settings-button.test.js deleted file mode 100644 index 8cc9778688a..00000000000 --- a/client/src/components/settings/settings-button.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import renderer from 'react-test-renderer'; - -import { - legacyProjectMap, - projectMap -} from '../../resources/cert-and-project-map'; -import { CertificationSettings } from './certification'; - -const props = { t: val => val }; - -const certificationSettings = new CertificationSettings(props); - -const { renderCertifications } = certificationSettings; - -beforeAll(() => { - projectMap['JavaScript Algorithms and Data Structures'] = projectMap[ - 'JavaScript Algorithms and Data Structures' - ].map(v => ({ - ...v, - link: 'javascript' - })); -}); - -it('should check legacy certification button consistency', () => { - const legacyCertifications = Object.keys(legacyProjectMap); - - const tree = renderer - .create( - legacyCertifications.map(certName => - renderCertifications(certName, legacyProjectMap) - ) - ) - .toJSON(); - - expect(tree).toMatchSnapshot(); -}); - -it('should check certification button consistency', () => { - const certifications = Object.keys(projectMap); - - const tree = renderer - .create( - certifications.map(certName => renderCertifications(certName, projectMap)) - ) - .toJSON(); - - expect(tree).toMatchSnapshot(); -}); diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index a6218f2d4e5..970ab54e7e9 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -1,7 +1,7 @@ import { HandlerProps } from 'react-reflex'; import { SuperBlocks } from '../../../config/certification-settings'; import { Themes } from '../components/settings/theme'; -import { certMap } from '../resources/cert-and-project-map'; +import { fullCertMap } from '../resources/cert-and-project-map'; export type Steps = { isHonest?: boolean; @@ -26,7 +26,7 @@ export type MarkdownRemark = { superBlock: SuperBlocks; // TODO: make enum like superBlock certification: string; - title: (typeof certMap)[number]['title']; + title: (typeof fullCertMap)[number]['title']; }; headings: [ { @@ -235,7 +235,7 @@ export type ProfileUI = { showTimeLine: boolean; }; -type ClaimedCertifications = { +export type ClaimedCertifications = { is2018DataVisCert: boolean; isApisMicroservicesCert: boolean; isBackEndCert: boolean; diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 6b7e27a1715..03ee76070dd 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -14,7 +14,7 @@ import { certTypes } from '../../../../config/certification-settings'; import { createFlashMessage } from '../../components/Flash/redux'; -import { certMap } from '../../resources/cert-and-project-map'; +import { fullCertMap } from '../../resources/cert-and-project-map'; import { getUsernameExists, putUpdateMyAbout, @@ -171,7 +171,7 @@ function* validateUsernameSaga({ payload }) { function* verifyCertificationSaga({ payload }) { // check redux if can claim cert before calling backend - const currentCert = certMap.find(cert => cert.certSlug === payload); + const currentCert = fullCertMap.find(cert => cert.certSlug === payload); const completedChallenges = yield select(completedChallengesSelector); const certTitle = currentCert?.title || payload; diff --git a/client/src/resources/cert-and-project-map.ts b/client/src/resources/cert-and-project-map.ts index 9da6d6aeb18..49bb1cab960 100644 --- a/client/src/resources/cert-and-project-map.ts +++ b/client/src/resources/cert-and-project-map.ts @@ -40,7 +40,7 @@ const legacyInfosecQaInfosecBase = infoSecBase; // TODO: generate this automatically in a separate file // from the md/meta.json files for each cert and projects -const certMap = [ +const legacyCertMap = [ { id: '561add10cb82ac38a17513be', title: 'Legacy Front End', @@ -177,14 +177,7 @@ const certMap = [ } ] }, - { - id: '561add10cb82ac38a17213bd', - title: 'Legacy Full Stack', - certSlug: 'full-stack', - flag: 'isFullStackCert' - // Requirements are other certs and is - // handled elsewhere - }, + { id: '561add10cb82ac39a17513bc', title: 'Legacy Data Visualization', @@ -292,7 +285,18 @@ const certMap = [ certSlug: 'information-security-and-quality-assurance' } ] - }, + } +] as const; +const legacyFullStack = { + id: '561add10cb82ac38a17213bd', + title: 'Legacy Full Stack', + certSlug: 'full-stack', + flag: 'isFullStackCert', + projects: null + // Requirements are other certs and is + // handled elsewhere +} as const; +const certMap = [ { id: '561add10cb82ac38a17513bc', title: 'Responsive Web Design', @@ -333,7 +337,6 @@ const certMap = [ } ] }, - { id: '561abd10cb81ac38a17513bc', title: 'JavaScript Algorithms and Data Structures', @@ -754,6 +757,7 @@ const certMap = [ ] } ] as const; +const upcomingCertMap = [] as const; function getResponsiveWebDesignPath(project: string) { return `${responsiveWeb22Base}/${project}-project/${project}`; @@ -769,23 +773,50 @@ function getJavaScriptAlgoPath(project: string) { : `${jsAlgoBase}/${project}`; } -const titles = certMap.map(({ title }) => title); -type Title = (typeof titles)[number]; -const legacyProjectMap: Partial> = {}; -const projectMap: Partial> = {}; +const certMapWithoutFullStack = [ + ...upcomingCertMap, + ...legacyCertMap, + ...certMap +] as const; -certMap.forEach(cert => { - // Filter out Legacy Full Stack so inputs for project - // URLs aren't rendered on the settings page - if (cert.title !== 'Legacy Full Stack') { - if (cert.title.startsWith('Legacy')) { - legacyProjectMap[cert.title] = cert.projects; - // temporary hiding of certs from settings page - // should do suggestion on line 33 and use front matter to hide it - } else { - projectMap[cert.title] = cert.projects; - } - } -}); +const fullCertMap = [...certMapWithoutFullStack, legacyFullStack] as const; -export { certMap, legacyProjectMap, projectMap }; +export type ProjectMap = Record< + (typeof certMap)[number]['title'], + (typeof certMap)[number]['projects'] +>; + +const projectMap = certMap.reduce((acc, curr) => { + return { + ...acc, + [curr.title]: curr.projects + }; +}, {} as ProjectMap); + +export type LegacyProjectMap = Record< + (typeof legacyCertMap)[number]['title'], + (typeof legacyCertMap)[number]['projects'] +>; + +const legacyProjectMap = legacyCertMap.reduce((acc, curr) => { + return { + ...acc, + [curr.title]: curr.projects + }; +}, {} as LegacyProjectMap); + +const fullProjectMap = { + ...legacyProjectMap, + ...projectMap +}; + +export { + certMap, + certMapWithoutFullStack, + fullCertMap, + fullProjectMap, + legacyCertMap, + legacyProjectMap, + projectMap, + upcomingCertMap +}; diff --git a/client/src/templates/Introduction/components/cert-challenge.tsx b/client/src/templates/Introduction/components/cert-challenge.tsx index a9cdd3a0de4..957e2f8c168 100644 --- a/client/src/templates/Introduction/components/cert-challenge.tsx +++ b/client/src/templates/Introduction/components/cert-challenge.tsx @@ -18,7 +18,7 @@ import { } from '../../../redux/selectors'; import { User, Steps } from '../../../redux/prop-types'; import { verifyCert } from '../../../redux/settings/actions'; -import { certMap } from '../../../resources/cert-and-project-map'; +import { fullCertMap } from '../../../resources/cert-and-project-map'; interface CertChallengeProps { // TODO: create enum/reuse SuperBlocks enum somehow @@ -33,7 +33,7 @@ interface CertChallengeProps { isSignedIn: boolean; currentCerts: Steps['currentCerts']; superBlock: SuperBlocks; - title: (typeof certMap)[number]['title']; + title: (typeof fullCertMap)[number]['title']; user: User; verifyCert: typeof verifyCert; } @@ -80,7 +80,7 @@ const CertChallenge = ({ const [userLoaded, setUserLoaded] = useState(false); // @ts-expect-error Typescript is confused - const certSlug = certMap.find(x => x.title === title).certSlug; + const certSlug = fullCertMap.find(x => x.title === title).certSlug; useEffect(() => { const { pending, complete } = fetchState;