feat(certs): Claim Certs

This commit is contained in:
Bouncey
2018-09-25 12:51:17 +01:00
committed by Stuart Taylor
parent d698d52794
commit 87837f480d
9 changed files with 309 additions and 92 deletions

View File

@ -34,7 +34,7 @@ export default function bootCertificate(app) {
const showCert = createShowCert(app); const showCert = createShowCert(app);
const verifyCert = createVerifyCert(certTypeIds, app); const verifyCert = createVerifyCert(certTypeIds, app);
api.post('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert); api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.get('/certificate/showCert/:username/:cert', showCert); api.get('/certificate/showCert/:username/:cert', showCert);
app.use('/internal', api); app.use('/internal', api);
@ -47,18 +47,18 @@ const noNameMessage = dedent`
`; `;
const notCertifiedMessage = name => dedent` const notCertifiedMessage = name => dedent`
it looks like you have not completed the necessary steps. It looks like you have not completed the necessary steps.
Please complete the required challenges to claim the Please complete the required projects to claim the
${name} ${name} Certification
`; `;
const alreadyClaimedMessage = name => dedent` const alreadyClaimedMessage = name => dedent`
It looks like you already have claimed the ${name} It looks like you already have claimed the ${name} Certification
`; `;
const successMessage = (username, name) => dedent` const successMessage = (username, name) => dedent`
@${username}, you have successfully claimed @${username}, you have successfully claimed
the ${name}! the ${name} Certification!
Congratulations on behalf of the freeCodeCamp.org team! Congratulations on behalf of the freeCodeCamp.org team!
`; `;
@ -194,6 +194,34 @@ function sendCertifiedEmail(
return send$(notifyUser).map(() => true); return send$(notifyUser).map(() => true);
} }
function getUserIsCertMap(user) {
const {
isRespWebDesignCert = false,
isJsAlgoDataStructCert = false,
isFrontEndLibsCert = false,
is2018DataVisCert = false,
isApisMicroservicesCert = false,
isInfosecQaCert = false,
isFrontEndCert = false,
isBackEndCert = false,
isDataVisCert = false,
isFullStackCert = false
} = user;
return {
isRespWebDesignCert,
isJsAlgoDataStructCert,
isFrontEndLibsCert,
is2018DataVisCert,
isApisMicroservicesCert,
isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert
};
}
function createVerifyCert(certTypeIds, app) { function createVerifyCert(certTypeIds, app) {
const { Email } = app.models; const { Email } = app.models;
return function verifyCert(req, res, next) { return function verifyCert(req, res, next) {
@ -264,8 +292,11 @@ function createVerifyCert(certTypeIds, app) {
}) })
.subscribe(message => { .subscribe(message => {
return res.status(200).json({ return res.status(200).json({
message, response: {
success: message.includes('Congratulations') type: message.includes('Congratulations') ? 'success' : 'info',
message
},
isCertMap: getUserIsCertMap(user)
}); });
}, next); }, next);
}; };

View File

@ -7,7 +7,8 @@ import { Grid, Button } from '@freecodecamp/react-bootstrap';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { signInLoadingSelector, userSelector } from '../redux'; import { signInLoadingSelector, userSelector } from '../redux';
import { submitNewAbout, updateUserFlag } from '../redux/settings'; import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
import { createFlashMessage } from '../components/Flash/redux';
import Layout from '../components/Layout'; import Layout from '../components/Layout';
import Spacer from '../components/helpers/Spacer'; import Spacer from '../components/helpers/Spacer';
@ -22,6 +23,7 @@ import Honesty from '../components/settings/Honesty';
import Certification from '../components/settings/Certification'; import Certification from '../components/settings/Certification';
const propTypes = { const propTypes = {
createFlashMessage: PropTypes.func.isRequired,
showLoading: PropTypes.bool, showLoading: PropTypes.bool,
submitNewAbout: PropTypes.func.isRequired, submitNewAbout: PropTypes.func.isRequired,
toggleNightMode: PropTypes.func.isRequired, toggleNightMode: PropTypes.func.isRequired,
@ -74,7 +76,8 @@ const propTypes = {
twitter: PropTypes.string, twitter: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
website: PropTypes.string website: PropTypes.string
}) }),
verifyCert: PropTypes.func.isRequired
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
@ -89,18 +92,21 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ {
createFlashMessage,
submitNewAbout, submitNewAbout,
toggleNightMode: theme => updateUserFlag({ theme }), toggleNightMode: theme => updateUserFlag({ theme }),
updateInternetSettings: updateUserFlag, updateInternetSettings: updateUserFlag,
updateIsHonest: updateUserFlag, updateIsHonest: updateUserFlag,
updatePortfolio: updateUserFlag, updatePortfolio: updateUserFlag,
updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail }) updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail }),
verifyCert
}, },
dispatch dispatch
); );
function ShowSettings(props) { function ShowSettings(props) {
const { const {
createFlashMessage,
submitNewAbout, submitNewAbout,
toggleNightMode, toggleNightMode,
user: { user: {
@ -115,6 +121,7 @@ function ShowSettings(props) {
isInfosecQaCert, isInfosecQaCert,
isFrontEndLibsCert, isFrontEndLibsCert,
isFullStackCert, isFullStackCert,
isRespWebDesignCert,
isEmailVerified, isEmailVerified,
isHonest, isHonest,
sendQuincyEmail, sendQuincyEmail,
@ -135,7 +142,8 @@ function ShowSettings(props) {
updateQuincyEmail, updateQuincyEmail,
updateInternetSettings, updateInternetSettings,
updatePortfolio, updatePortfolio,
updateIsHonest updateIsHonest,
verifyCert
} = props; } = props;
if (showLoading) { if (showLoading) {
@ -212,6 +220,7 @@ function ShowSettings(props) {
<Spacer /> <Spacer />
<Certification <Certification
completedChallenges={completedChallenges} completedChallenges={completedChallenges}
createFlashMessage={createFlashMessage}
is2018DataVisCert={is2018DataVisCert} is2018DataVisCert={is2018DataVisCert}
isApisMicroservicesCert={isApisMicroservicesCert} isApisMicroservicesCert={isApisMicroservicesCert}
isBackEndCert={isBackEndCert} isBackEndCert={isBackEndCert}
@ -219,8 +228,12 @@ function ShowSettings(props) {
isFrontEndCert={isFrontEndCert} isFrontEndCert={isFrontEndCert}
isFrontEndLibsCert={isFrontEndLibsCert} isFrontEndLibsCert={isFrontEndLibsCert}
isFullStackCert={isFullStackCert} isFullStackCert={isFullStackCert}
isHonest={isHonest}
isInfosecQaCert={isInfosecQaCert} isInfosecQaCert={isInfosecQaCert}
isJsAlgoDataStructCert={isJsAlgoDataStructCert} isJsAlgoDataStructCert={isJsAlgoDataStructCert}
isRespWebDesignCert={isRespWebDesignCert}
username={username}
verifyCert={verifyCert}
/> />
<Spacer /> <Spacer />
{/* <DangerZone /> */} {/* <DangerZone /> */}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { find } from 'lodash'; import { find, first } from 'lodash';
import { import {
Table, Table,
Button, Button,
@ -9,14 +9,17 @@ import {
Modal Modal
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import { Link, navigate } from 'gatsby'; import { Link, navigate } from 'gatsby';
import { createSelector } from 'reselect';
import { projectMap } from '../../resources/certProjectMap'; import { projectMap } from '../../resources/certProjectMap';
import SectionHeader from './SectionHeader'; import SectionHeader from './SectionHeader';
import SolutionViewer from './SolutionViewer'; import SolutionViewer from './SolutionViewer';
import { FullWidthRow } from '../helpers'; import { FullWidthRow, Spacer } from '../helpers';
import { maybeUrlRE } from '../../utils'; import { maybeUrlRE } from '../../utils';
import './certification.css';
const propTypes = { const propTypes = {
completedChallenges: PropTypes.arrayOf( completedChallenges: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
@ -27,10 +30,71 @@ const propTypes = {
completedDate: PropTypes.number, completedDate: PropTypes.number,
files: PropTypes.array files: PropTypes.array
}) })
) ),
createFlashMessage: PropTypes.func.isRequired,
is2018DataVisCert: PropTypes.bool,
isApisMicroservicesCert: PropTypes.bool,
isBackEndCert: PropTypes.bool,
isDataVisCert: PropTypes.bool,
isFrontEndCert: PropTypes.bool,
isFrontEndLibsCert: PropTypes.bool,
isFullStackCert: PropTypes.bool,
isHonest: PropTypes.bool,
isInfosecQaCert: PropTypes.bool,
isJsAlgoDataStructCert: PropTypes.bool,
isRespWebDesignCert: PropTypes.bool,
username: PropTypes.string,
verifyCert: PropTypes.func.isRequired
}; };
const certifications = Object.keys(projectMap); const certifications = Object.keys(projectMap);
const isCertSelector = ({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
}) => ({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
});
const isCertMapSelector = createSelector(
isCertSelector,
({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
}) => ({
'Responsive Web Design': isRespWebDesignCert,
'JavaScript Algorithms and Data Structures': isJsAlgoDataStructCert,
'Front End Libraries': isFrontEndLibsCert,
'Data Visualization': is2018DataVisCert,
"API's and Microservices": isApisMicroservicesCert,
'Information Security And Quality Assurance': isInfosecQaCert
})
);
const initialState = { const initialState = {
solutionViewer: { solutionViewer: {
projectTitle: '', projectTitle: '',
@ -51,8 +115,11 @@ class CertificationSettings extends Component {
e.preventDefault(); e.preventDefault();
return navigate(to); return navigate(to);
}; };
handleSolutionModalHide = () => this.setState({ ...initialState }); handleSolutionModalHide = () => this.setState({ ...initialState });
getUserIsCertMap = () => isCertMapSelector(this.props);
getProjectSolution = (projectId, projectTitle) => { getProjectSolution = (projectId, projectTitle) => {
const { completedChallenges } = this.props; const { completedChallenges } = this.props;
const completedProject = find( const completedProject = find(
@ -75,6 +142,7 @@ class CertificationSettings extends Component {
if (files && files.length) { if (files && files.length) {
return ( return (
<Button <Button
block={true}
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
onClick={onClickHandler} onClick={onClickHandler}
@ -85,34 +153,38 @@ class CertificationSettings extends Component {
} }
if (githubLink) { if (githubLink) {
return ( return (
<DropdownButton <div className='solutions-dropdown'>
bsStyle='primary' <DropdownButton
className='btn-invert' block={true}
id={`dropdown-for-${projectId}`}
title='Show Solutions'
>
<MenuItem
bsStyle='primary' bsStyle='primary'
href={solution} className='btn-invert'
rel='noopener noreferrer' id={`dropdown-for-${projectId}`}
target='_blank' title='Show Solutions'
> >
Front End <MenuItem
</MenuItem> bsStyle='primary'
<MenuItem href={solution}
bsStyle='primary' rel='noopener noreferrer'
href={githubLink} target='_blank'
rel='noopener noreferrer' >
target='_blank' Front End
> </MenuItem>
Back End <MenuItem
</MenuItem> bsStyle='primary'
</DropdownButton> href={githubLink}
rel='noopener noreferrer'
target='_blank'
>
Back End
</MenuItem>
</DropdownButton>
</div>
); );
} }
if (maybeUrlRE.test(solution)) { if (maybeUrlRE.test(solution)) {
return ( return (
<Button <Button
block={true}
bsStyle='primary' bsStyle='primary'
className='btn-invert' className='btn-invert'
href={solution} href={solution}
@ -124,7 +196,12 @@ class CertificationSettings extends Component {
); );
} }
return ( return (
<Button bsStyle='primary' className='btn-invert' onClick={onClickHandler}> <Button
block={true}
bsStyle='primary'
className='btn-invert'
onClick={onClickHandler}
>
Show Code Show Code
</Button> </Button>
); );
@ -132,6 +209,7 @@ class CertificationSettings extends Component {
renderCertifications = certName => ( renderCertifications = certName => (
<FullWidthRow key={certName}> <FullWidthRow key={certName}>
<Spacer />
<h3>{certName}</h3> <h3>{certName}</h3>
<Table> <Table>
<thead> <thead>
@ -140,22 +218,57 @@ class CertificationSettings extends Component {
<th>Solution</th> <th>Solution</th>
</tr> </tr>
</thead> </thead>
<tbody>{this.renderProjectsFor(certName)}</tbody> <tbody>
{this.renderProjectsFor(certName, this.getUserIsCertMap()[certName])}
</tbody>
</Table> </Table>
</FullWidthRow> </FullWidthRow>
); );
renderProjectsFor = certName => renderProjectsFor = (certName, isCert) => {
projectMap[certName].map(({ link, title, id }) => ( const { username, isHonest, createFlashMessage, verifyCert } = this.props;
<tr className='project-row' key={id}> const { superBlock } = first(projectMap[certName]);
<td className='project-title col-sm-8'> const certLocation = `/certification/${username}/${superBlock}`;
<Link to={link}>{title}</Link> const createClickHandler = superBlock => e => {
</td> e.preventDefault();
<td className='project-solution col-sm-4'> if (isCert) {
{this.getProjectSolution(id, title)} return navigate(certLocation);
</td> }
</tr> return isHonest
)); ? verifyCert(superBlock)
: createFlashMessage({
type: 'info',
message:
'To claim a certification, you must first accept our acedemic ' +
'honesty policy'
});
};
return projectMap[certName]
.map(({ link, title, id }) => (
<tr className='project-row' key={id}>
<td className='project-title col-sm-8'>
<Link to={link}>{title}</Link>
</td>
<td className='project-solution col-sm-4'>
{this.getProjectSolution(id, title)}
</td>
</tr>
))
.concat([
<tr key={`cert-${superBlock}-button`}>
<td colSpan={2}>
<Button
block={true}
bsStyle='primary'
href={certLocation}
onClick={createClickHandler(superBlock)}
>
{isCert ? 'Show Certification' : 'Claim Certification'}
</Button>
</td>
</tr>
]);
};
render() { render() {
const { const {

View File

@ -1,13 +1,13 @@
#certifcation-settings .project-title { #certifcation-settings .solutions-dropdown,
display: flex; #certifcation-settings .solutions-dropdown .dropdown-menu,
#certifcation-settings .solutions-dropdown .dropdown {
width: 100%;
} }
#certifcation-settings .project-solution { #certifcation-settings tr {
display: flex; height: 57px;
} }
#certifcation-settings .project-row { #certifcation-settings .project-title > a {
display: flex; line-height: 40px;
}
}

View File

@ -174,6 +174,8 @@ export const reducer = handleActions(
[settingsTypes.updateMyEmailComplete]: (state, { payload }) => [settingsTypes.updateMyEmailComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state, payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateUserFlagComplete]: (state, { payload }) => [settingsTypes.updateUserFlagComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.verifyCertComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state payload ? spreadThePayloadOnUser(state, payload) : state
}, },
initialState initialState

View File

@ -27,7 +27,8 @@ export const types = createTypes(
...createAsyncTypes('submitNewUsername'), ...createAsyncTypes('submitNewUsername'),
...createAsyncTypes('updateMyEmail'), ...createAsyncTypes('updateMyEmail'),
...createAsyncTypes('updateUserFlag'), ...createAsyncTypes('updateUserFlag'),
...createAsyncTypes('submitProfileUI') ...createAsyncTypes('submitProfileUI'),
...createAsyncTypes('verifyCert')
], ],
ns ns
); );
@ -80,6 +81,13 @@ export const validateUsernameComplete = createAction(
); );
export const validateUsernameError = createAction(types.validateUsernameError); export const validateUsernameError = createAction(types.validateUsernameError);
export const verifyCert = createAction(types.verifyCert);
export const verifyCertComplete = createAction(
types.verifyCertComplete,
checkForSuccessPayload
);
export const verifyCertError = createAction(types.verifyCertError);
export const usernameValidationSelector = state => state[ns].usernameValidation; export const usernameValidationSelector = state => state[ns].usernameValidation;
export const reducer = handleActions( export const reducer = handleActions(

View File

@ -11,14 +11,17 @@ import {
submitNewUsernameComplete, submitNewUsernameComplete,
submitNewUsernameError, submitNewUsernameError,
submitProfileUIComplete, submitProfileUIComplete,
submitProfileUIError submitProfileUIError,
verifyCertComplete,
verifyCertError
} from './'; } from './';
import { import {
getUsernameExists, getUsernameExists,
putUpdateMyAbout, putUpdateMyAbout,
putUpdateMyProfileUI, putUpdateMyProfileUI,
putUpdateMyUsername, putUpdateMyUsername,
putUpdateUserFlag putUpdateUserFlag,
putVerifyCert
} from '../../utils/ajax'; } from '../../utils/ajax';
import { createFlashMessage } from '../../components/Flash/redux'; import { createFlashMessage } from '../../components/Flash/redux';
@ -74,12 +77,25 @@ function* validateUsernameSaga({ payload }) {
} }
} }
function* verifyCertificationSaga({ payload }) {
try {
const {
data: { response, isCertMap }
} = yield call(putVerifyCert, payload);
yield put(verifyCertComplete({ ...response, payload: isCertMap }));
yield put(createFlashMessage(response));
} catch (e) {
yield put(verifyCertError(e));
}
}
export function createSettingsSagas(types) { export function createSettingsSagas(types) {
return [ return [
takeEvery(types.updateUserFlag, updateUserFlagSaga), takeEvery(types.updateUserFlag, updateUserFlagSaga),
takeLatest(types.submitNewAbout, submitNewAboutSaga), takeLatest(types.submitNewAbout, submitNewAboutSaga),
takeLatest(types.submitNewUsername, submitNewUsernameSaga), takeLatest(types.submitNewUsername, submitNewUsernameSaga),
takeLatest(types.validateUsername, validateUsernameSaga), takeLatest(types.validateUsername, validateUsernameSaga),
takeLatest(types.submitProfileUI, sumbitProfileUISaga) takeLatest(types.submitProfileUI, sumbitProfileUISaga),
takeEvery(types.verifyCert, verifyCertificationSaga)
]; ];
} }

View File

@ -16,162 +16,192 @@ export const projectMap = {
{ {
id: 'bd7158d8c442eddfaeb5bd18', id: 'bd7158d8c442eddfaeb5bd18',
title: 'Build a Tribute Page', title: 'Build a Tribute Page',
link: `${responsiveWebBase}/build-a-tribute-page` link: `${responsiveWebBase}/build-a-tribute-page`,
superBlock: 'responsive-web-design'
}, },
{ {
id: '587d78af367417b2b2512b03', id: '587d78af367417b2b2512b03',
title: 'Build a Survey Form', title: 'Build a Survey Form',
link: `${responsiveWebBase}/build-a-survey-form` link: `${responsiveWebBase}/build-a-survey-form`,
superBlock: 'responsive-web-design'
}, },
{ {
id: '587d78af367417b2b2512b04', id: '587d78af367417b2b2512b04',
title: 'Build a Product Landing Page', title: 'Build a Product Landing Page',
link: `${responsiveWebBase}/build-a-product-landing-page` link: `${responsiveWebBase}/build-a-product-landing-page`,
superBlock: 'responsive-web-design'
}, },
{ {
id: '587d78b0367417b2b2512b05', id: '587d78b0367417b2b2512b05',
title: 'Build a Technical Documentation Page', title: 'Build a Technical Documentation Page',
link: `${responsiveWebBase}/build-a-technical-documentation-page` link: `${responsiveWebBase}/build-a-technical-documentation-page`,
superBlock: 'responsive-web-design'
}, },
{ {
id: 'bd7158d8c242eddfaeb5bd13', id: 'bd7158d8c242eddfaeb5bd13',
title: 'Build a Personal Portfolio Webpage', title: 'Build a Personal Portfolio Webpage',
link: `${responsiveWebBase}/build-a-personal-portfolio-webpage` link: `${responsiveWebBase}/build-a-personal-portfolio-webpage`,
superBlock: 'responsive-web-design'
} }
], ],
'JavaScript Algorithms and Data Structures': [ 'JavaScript Algorithms and Data Structures': [
{ {
id: 'aaa48de84e1ecc7c742e1124', id: 'aaa48de84e1ecc7c742e1124',
title: 'Palindrome Checker', title: 'Palindrome Checker',
link: `${jsAlgoBase}/palindrome-checker` link: `${jsAlgoBase}/palindrome-checker`,
superBlock: 'javascript-algorithms-and-data-structures'
}, },
{ {
id: 'a7f4d8f2483413a6ce226cac', id: 'a7f4d8f2483413a6ce226cac',
title: 'Roman Numeral Converter', title: 'Roman Numeral Converter',
link: `${jsAlgoBase}/roman-numeral-converter` link: `${jsAlgoBase}/roman-numeral-converter`,
superBlock: 'javascript-algorithms-and-data-structures'
}, },
{ {
id: '56533eb9ac21ba0edf2244e2', id: '56533eb9ac21ba0edf2244e2',
title: 'Caesars Cipher', title: 'Caesars Cipher',
link: `${jsAlgoBase}/caesars-cipher` link: `${jsAlgoBase}/caesars-cipher`,
superBlock: 'javascript-algorithms-and-data-structures'
}, },
{ {
id: 'aff0395860f5d3034dc0bfc9', id: 'aff0395860f5d3034dc0bfc9',
title: 'Telephone Number Validator', title: 'Telephone Number Validator',
link: `${jsAlgoBase}/telephone-number-validator` link: `${jsAlgoBase}/telephone-number-validator`,
superBlock: 'javascript-algorithms-and-data-structures'
}, },
{ {
id: 'aa2e6f85cab2ab736c9a9b24', id: 'aa2e6f85cab2ab736c9a9b24',
title: 'Cash Register', title: 'Cash Register',
link: `${jsAlgoBase}/cash-register` link: `${jsAlgoBase}/cash-register`,
superBlock: 'javascript-algorithms-and-data-structures'
} }
], ],
'Front End Libraries': [ 'Front End Libraries': [
{ {
id: 'bd7158d8c442eddfaeb5bd13', id: 'bd7158d8c442eddfaeb5bd13',
title: 'Build a Random Quote Machine', title: 'Build a Random Quote Machine',
link: `${feLibsBase}/build-a-random-quote-machine` link: `${feLibsBase}/build-a-random-quote-machine`,
superBlock: 'front-end-libraries'
}, },
{ {
id: 'bd7157d8c242eddfaeb5bd13', id: 'bd7157d8c242eddfaeb5bd13',
title: 'Build a Markdown Previewer', title: 'Build a Markdown Previewer',
link: `${feLibsBase}/build-a-markdown-previewer` link: `${feLibsBase}/build-a-markdown-previewer`,
superBlock: 'front-end-libraries'
}, },
{ {
id: '587d7dbc367417b2b2512bae', id: '587d7dbc367417b2b2512bae',
title: 'Build a Drum Machine', title: 'Build a Drum Machine',
link: `${feLibsBase}/build-a-drum-machine` link: `${feLibsBase}/build-a-drum-machine`,
superBlock: 'front-end-libraries'
}, },
{ {
id: 'bd7158d8c442eddfaeb5bd17', id: 'bd7158d8c442eddfaeb5bd17',
title: 'Build a JavaScript Calculator', title: 'Build a JavaScript Calculator',
link: `${feLibsBase}/build-a-javascript-calculator` link: `${feLibsBase}/build-a-javascript-calculator`,
superBlock: 'front-end-libraries'
}, },
{ {
id: 'bd7158d8c442eddfaeb5bd0f', id: 'bd7158d8c442eddfaeb5bd0f',
title: 'Build a Pomodoro Clock', title: 'Build a Pomodoro Clock',
link: `${feLibsBase}/build-a-pomodoro-clock` link: `${feLibsBase}/build-a-pomodoro-clock`,
superBlock: 'front-end-libraries'
} }
], ],
'Data Visualization': [ 'Data Visualization': [
{ {
id: 'bd7168d8c242eddfaeb5bd13', id: 'bd7168d8c242eddfaeb5bd13',
title: 'Visualize Data with a Bar Chart', title: 'Visualize Data with a Bar Chart',
link: `${dataVisBase}/visualize-data-with-a-bar-chart` link: `${dataVisBase}/visualize-data-with-a-bar-chart`,
superBlock: 'data-visualization'
}, },
{ {
id: 'bd7178d8c242eddfaeb5bd13', id: 'bd7178d8c242eddfaeb5bd13',
title: 'Visualize Data with a Scatterplot Graph', title: 'Visualize Data with a Scatterplot Graph',
link: `${dataVisBase}/visualize-data-with-a-scatterplot-graph` link: `${dataVisBase}/visualize-data-with-a-scatterplot-graph`,
superBlock: 'data-visualization'
}, },
{ {
id: 'bd7188d8c242eddfaeb5bd13', id: 'bd7188d8c242eddfaeb5bd13',
title: 'Visualize Data with a Heat Map', title: 'Visualize Data with a Heat Map',
link: `${dataVisBase}/visualize-data-with-a-heat-map` link: `${dataVisBase}/visualize-data-with-a-heat-map`,
superBlock: 'data-visualization'
}, },
{ {
id: '587d7fa6367417b2b2512bbf', id: '587d7fa6367417b2b2512bbf',
title: 'Visualize Data with a Choropleth Map', title: 'Visualize Data with a Choropleth Map',
link: `${dataVisBase}/visualize-data-with-a-choropleth-map` link: `${dataVisBase}/visualize-data-with-a-choropleth-map`,
superBlock: 'data-visualization'
}, },
{ {
id: '587d7fa6367417b2b2512bc0', id: '587d7fa6367417b2b2512bc0',
title: 'Visualize Data with a Treemap Diagram', title: 'Visualize Data with a Treemap Diagram',
link: `${dataVisBase}/visualize-data-with-a-treemap-diagram` link: `${dataVisBase}/visualize-data-with-a-treemap-diagram`,
superBlock: 'data-visualization'
} }
], ],
"API's and Microservices": [ "API's and Microservices": [
{ {
id: 'bd7158d8c443edefaeb5bdef', id: 'bd7158d8c443edefaeb5bdef',
title: 'Timestamp Microservice', title: 'Timestamp Microservice',
link: `${apiMicroBase}/timestamp-microservice` link: `${apiMicroBase}/timestamp-microservice`,
superBlock: 'apis-and-microservices'
}, },
{ {
id: 'bd7158d8c443edefaeb5bdff', id: 'bd7158d8c443edefaeb5bdff',
title: 'Request Header Parser Microservice', title: 'Request Header Parser Microservice',
link: `${apiMicroBase}/request-header-parser-microservice` link: `${apiMicroBase}/request-header-parser-microservice`,
superBlock: 'apis-and-microservices'
}, },
{ {
id: 'bd7158d8c443edefaeb5bd0e', id: 'bd7158d8c443edefaeb5bd0e',
title: 'URL Shortener Microservice', title: 'URL Shortener Microservice',
link: `${apiMicroBase}/url-shortener-microservice` link: `${apiMicroBase}/url-shortener-microservice`,
superBlock: 'apis-and-microservices'
}, },
{ {
id: '5a8b073d06fa14fcfde687aa', id: '5a8b073d06fa14fcfde687aa',
title: 'Exercise Tracker', title: 'Exercise Tracker',
link: `${apiMicroBase}/exercise-tracker` link: `${apiMicroBase}/exercise-tracker`,
superBlock: 'apis-and-microservices'
}, },
{ {
id: 'bd7158d8c443edefaeb5bd0f', id: 'bd7158d8c443edefaeb5bd0f',
title: 'File Metadata Microservice', title: 'File Metadata Microservice',
link: `${apiMicroBase}/file-metadata-microservice` link: `${apiMicroBase}/file-metadata-microservice`,
superBlock: 'apis-and-microservices'
} }
], ],
'Information Security And Quality Assurance': [ 'Information Security And Quality Assurance': [
{ {
id: '587d8249367417b2b2512c41', id: '587d8249367417b2b2512c41',
title: 'Metric-Imperial Converter', title: 'Metric-Imperial Converter',
link: `${infoSecBase}/metric-imperial-converter` link: `${infoSecBase}/metric-imperial-converter`,
superBlock: 'information-security-and-quality-assurance'
}, },
{ {
id: '587d8249367417b2b2512c42', id: '587d8249367417b2b2512c42',
title: 'Issue Tracker', title: 'Issue Tracker',
link: `${infoSecBase}/issue-tracker` link: `${infoSecBase}/issue-tracker`,
superBlock: 'information-security-and-quality-assurance'
}, },
{ {
id: '587d824a367417b2b2512c43', id: '587d824a367417b2b2512c43',
title: 'Personal Library', title: 'Personal Library',
link: `${infoSecBase}/personal-library` link: `${infoSecBase}/personal-library`,
superBlock: 'information-security-and-quality-assurance'
}, },
{ {
id: '587d824a367417b2b2512c44', id: '587d824a367417b2b2512c44',
title: 'Stock Price Checker', title: 'Stock Price Checker',
link: `${infoSecBase}/stock-price-checker` link: `${infoSecBase}/stock-price-checker`,
superBlock: 'information-security-and-quality-assurance'
}, },
{ {
id: '587d824a367417b2b2512c45', id: '587d824a367417b2b2512c45',
title: 'Anonymous Message Board', title: 'Anonymous Message Board',
link: `${infoSecBase}/anonymous-message-board` link: `${infoSecBase}/anonymous-message-board`,
superBlock: 'information-security-and-quality-assurance'
} }
] ]
}; };

View File

@ -64,4 +64,8 @@ export function putUserUpdateEmail(email) {
return put('/update-my-email', { email }); return put('/update-my-email', { email });
} }
export function putVerifyCert(superBlock) {
return put('/certificate/verify', { superBlock });
}
/** DELETE **/ /** DELETE **/