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 <muhammed@freecodecamp.org>

* refactor: use exported map types

Co-authored-by: Muhammed Mustafa <muhammed@freecodecamp.org>

---------

Co-authored-by: Muhammed Mustafa <muhammed@freecodecamp.org>
This commit is contained in:
Shaun Hamilton
2023-04-28 09:01:44 +01:00
committed by GitHub
parent 44ae9bd0c5
commit 3af73290bc
10 changed files with 302 additions and 2759 deletions

View File

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

View File

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

View File

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

View File

@@ -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<keyof ProjectMap>;
// 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<User, 'completedChallenges' | 'isHonest' | 'username'>;
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<CompletedChallenge | null>(
null
);
const [solution, setSolution] = useState<string | null>();
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 (
<FullWidthRow key={certName}>
<Spacer size='medium' />
@@ -243,22 +223,26 @@ export class CertificationSettings extends Component {
</tr>
</thead>
<tbody>
{this.renderProjectsFor(
{renderProjectsFor({
certName,
this.getUserIsCertMap()[certName],
projectsMap
)}
isCert: getUserIsCertMap()[certName]
})}
</tbody>
</Table>
</FullWidthRow>
);
};
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<HTMLButtonElement>) => {
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 }) => (
<tr className='project-row' key={id}>
<td className='project-title col-sm-8 col-xs-8'>
@@ -276,7 +260,7 @@ export class CertificationSettings extends Component {
</Link>
</td>
<td className='project-solution col-sm-4 col-xs-4'>
{this.getProjectSolution(id, title)}
{getProjectSolution(id, title)}
</td>
</tr>
))
@@ -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')}{' '}
<span className='sr-only'>{certName}</span>
@@ -297,9 +281,9 @@ export class CertificationSettings extends Component {
</td>
</tr>
]);
};
}
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<HTMLButtonElement>) => {
e.preventDefault();
if (isFullStackCert) {
return navigate(certLocation);
}
return isHonest
? verifyCert(certSlug)
: createFlashMessage(honestyInfoMessage);
};
return (
<FullWidthRow key={certSlug}>
<Spacer size='medium' />
@@ -408,39 +394,37 @@ export class CertificationSettings extends Component {
);
};
render() {
const { solutionViewer, projectViewer } = this.state;
const { t } = this.props;
const { t } = props;
return (
<ScrollableAnchor id='certification-settings'>
<section className='certification-settings'>
<SectionHeader>{t('settings.headings.certs')}</SectionHeader>
{certifications.map(certName =>
this.renderCertifications(certName, projectMap)
)}
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
{this.renderLegacyFullStack()}
{legacyCertifications.map(certName =>
this.renderCertifications(certName, legacyProjectMap)
)}
<ProjectModal
{...solutionViewer}
handleSolutionModalHide={this.handleSolutionModalHide}
/>
<ProjectPreviewModal
{...projectViewer}
closeText={t('buttons.close')}
showProjectPreview={true}
/>
</section>
</ScrollableAnchor>
);
}
return (
<ScrollableAnchor id='certification-settings'>
<section className='certification-settings'>
<SectionHeader>{t('settings.headings.certs')}</SectionHeader>
{certifications.map(certName => renderCertifications(certName))}
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
{renderLegacyFullStack()}
{legacyCertifications.map(certName => renderCertifications(certName))}
<ProjectModal
{...{
projectTitle,
challengeFiles,
solution: solution ?? undefined,
isOpen
}}
handleSolutionModalHide={handleSolutionModalHide}
/>
<ProjectPreviewModal
challengeData={challengeData}
previewTitle={projectTitle}
closeText={t('buttons.close')}
showProjectPreview={true}
/>
</section>
</ScrollableAnchor>
);
}
CertificationSettings.displayName = 'CertificationSettings';
CertificationSettings.propTypes = propTypes;
export default connect(
null,

View File

@@ -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();
});

View File

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

View File

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

View File

@@ -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<Record<Title, unknown>> = {};
const projectMap: Partial<Record<Title, unknown>> = {};
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
};

View File

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