diff --git a/api-server/server/boot/certificate.js b/api-server/server/boot/certificate.js index 6961366334..22f71cdff3 100644 --- a/api-server/server/boot/certificate.js +++ b/api-server/server/boot/certificate.js @@ -34,7 +34,7 @@ export default function bootCertificate(app) { const showCert = createShowCert(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); app.use('/internal', api); @@ -47,18 +47,18 @@ const noNameMessage = dedent` `; const notCertifiedMessage = name => dedent` - it looks like you have not completed the necessary steps. - Please complete the required challenges to claim the - ${name} + It looks like you have not completed the necessary steps. + Please complete the required projects to claim the + ${name} Certification `; 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` @${username}, you have successfully claimed - the ${name}! + the ${name} Certification! Congratulations on behalf of the freeCodeCamp.org team! `; @@ -194,6 +194,34 @@ function sendCertifiedEmail( 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) { const { Email } = app.models; return function verifyCert(req, res, next) { @@ -264,8 +292,11 @@ function createVerifyCert(certTypeIds, app) { }) .subscribe(message => { return res.status(200).json({ - message, - success: message.includes('Congratulations') + response: { + type: message.includes('Congratulations') ? 'success' : 'info', + message + }, + isCertMap: getUserIsCertMap(user) }); }, next); }; diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index de8f97daf3..d0d67e6f45 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -7,7 +7,8 @@ import { Grid, Button } from '@freecodecamp/react-bootstrap'; import Helmet from 'react-helmet'; 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 Spacer from '../components/helpers/Spacer'; @@ -22,6 +23,7 @@ import Honesty from '../components/settings/Honesty'; import Certification from '../components/settings/Certification'; const propTypes = { + createFlashMessage: PropTypes.func.isRequired, showLoading: PropTypes.bool, submitNewAbout: PropTypes.func.isRequired, toggleNightMode: PropTypes.func.isRequired, @@ -74,7 +76,8 @@ const propTypes = { twitter: PropTypes.string, username: PropTypes.string, website: PropTypes.string - }) + }), + verifyCert: PropTypes.func.isRequired }; const mapStateToProps = createSelector( @@ -89,18 +92,21 @@ const mapStateToProps = createSelector( const mapDispatchToProps = dispatch => bindActionCreators( { + createFlashMessage, submitNewAbout, toggleNightMode: theme => updateUserFlag({ theme }), updateInternetSettings: updateUserFlag, updateIsHonest: updateUserFlag, updatePortfolio: updateUserFlag, - updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail }) + updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail }), + verifyCert }, dispatch ); function ShowSettings(props) { const { + createFlashMessage, submitNewAbout, toggleNightMode, user: { @@ -115,6 +121,7 @@ function ShowSettings(props) { isInfosecQaCert, isFrontEndLibsCert, isFullStackCert, + isRespWebDesignCert, isEmailVerified, isHonest, sendQuincyEmail, @@ -135,7 +142,8 @@ function ShowSettings(props) { updateQuincyEmail, updateInternetSettings, updatePortfolio, - updateIsHonest + updateIsHonest, + verifyCert } = props; if (showLoading) { @@ -212,6 +220,7 @@ function ShowSettings(props) { {/* */} diff --git a/client/src/components/settings/Certification.js b/client/src/components/settings/Certification.js index 6b5df019fb..fff3f26eb1 100644 --- a/client/src/components/settings/Certification.js +++ b/client/src/components/settings/Certification.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { find } from 'lodash'; +import { find, first } from 'lodash'; import { Table, Button, @@ -9,14 +9,17 @@ import { Modal } from '@freecodecamp/react-bootstrap'; import { Link, navigate } from 'gatsby'; +import { createSelector } from 'reselect'; import { projectMap } from '../../resources/certProjectMap'; import SectionHeader from './SectionHeader'; import SolutionViewer from './SolutionViewer'; -import { FullWidthRow } from '../helpers'; +import { FullWidthRow, Spacer } from '../helpers'; import { maybeUrlRE } from '../../utils'; +import './certification.css'; + const propTypes = { completedChallenges: PropTypes.arrayOf( PropTypes.shape({ @@ -27,10 +30,71 @@ const propTypes = { completedDate: PropTypes.number, 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 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 = { solutionViewer: { projectTitle: '', @@ -51,8 +115,11 @@ class CertificationSettings extends Component { e.preventDefault(); return navigate(to); }; + handleSolutionModalHide = () => this.setState({ ...initialState }); + getUserIsCertMap = () => isCertMapSelector(this.props); + getProjectSolution = (projectId, projectTitle) => { const { completedChallenges } = this.props; const completedProject = find( @@ -75,6 +142,7 @@ class CertificationSettings extends Component { if (files && files.length) { return ( - + - Front End - - - Back End - - + + Front End + + + Back End + + + ); } if (maybeUrlRE.test(solution)) { return ( + Show Code ); @@ -132,6 +209,7 @@ class CertificationSettings extends Component { renderCertifications = certName => ( + {certName} @@ -140,22 +218,57 @@ class CertificationSettings extends Component { Solution - {this.renderProjectsFor(certName)} + + {this.renderProjectsFor(certName, this.getUserIsCertMap()[certName])} + ); - renderProjectsFor = certName => - projectMap[certName].map(({ link, title, id }) => ( - - - {title} - - - {this.getProjectSolution(id, title)} - - - )); + renderProjectsFor = (certName, isCert) => { + const { username, isHonest, createFlashMessage, verifyCert } = this.props; + const { superBlock } = first(projectMap[certName]); + const certLocation = `/certification/${username}/${superBlock}`; + const createClickHandler = superBlock => e => { + e.preventDefault(); + if (isCert) { + return navigate(certLocation); + } + 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 }) => ( + + + {title} + + + {this.getProjectSolution(id, title)} + + + )) + .concat([ + + + + {isCert ? 'Show Certification' : 'Claim Certification'} + + + + ]); + }; render() { const { diff --git a/client/src/components/settings/certification.css b/client/src/components/settings/certification.css index fa9d50b5d3..2f948e3e15 100644 --- a/client/src/components/settings/certification.css +++ b/client/src/components/settings/certification.css @@ -1,13 +1,13 @@ -#certifcation-settings .project-title { - display: flex; +#certifcation-settings .solutions-dropdown, +#certifcation-settings .solutions-dropdown .dropdown-menu, +#certifcation-settings .solutions-dropdown .dropdown { + width: 100%; } -#certifcation-settings .project-solution { - display: flex; - +#certifcation-settings tr { + height: 57px; } -#certifcation-settings .project-row { - display: flex; - -} \ No newline at end of file +#certifcation-settings .project-title > a { + line-height: 40px; +} diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 54e2481b7e..52e003ae46 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -174,6 +174,8 @@ export const reducer = handleActions( [settingsTypes.updateMyEmailComplete]: (state, { payload }) => payload ? spreadThePayloadOnUser(state, payload) : state, [settingsTypes.updateUserFlagComplete]: (state, { payload }) => + payload ? spreadThePayloadOnUser(state, payload) : state, + [settingsTypes.verifyCertComplete]: (state, { payload }) => payload ? spreadThePayloadOnUser(state, payload) : state }, initialState diff --git a/client/src/redux/settings/index.js b/client/src/redux/settings/index.js index 59deefaf4b..1b00e02342 100644 --- a/client/src/redux/settings/index.js +++ b/client/src/redux/settings/index.js @@ -27,7 +27,8 @@ export const types = createTypes( ...createAsyncTypes('submitNewUsername'), ...createAsyncTypes('updateMyEmail'), ...createAsyncTypes('updateUserFlag'), - ...createAsyncTypes('submitProfileUI') + ...createAsyncTypes('submitProfileUI'), + ...createAsyncTypes('verifyCert') ], ns ); @@ -80,6 +81,13 @@ export const validateUsernameComplete = createAction( ); 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 reducer = handleActions( diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 0a5b0b5dfc..df54b8990b 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -11,14 +11,17 @@ import { submitNewUsernameComplete, submitNewUsernameError, submitProfileUIComplete, - submitProfileUIError + submitProfileUIError, + verifyCertComplete, + verifyCertError } from './'; import { getUsernameExists, putUpdateMyAbout, putUpdateMyProfileUI, putUpdateMyUsername, - putUpdateUserFlag + putUpdateUserFlag, + putVerifyCert } from '../../utils/ajax'; 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) { return [ takeEvery(types.updateUserFlag, updateUserFlagSaga), takeLatest(types.submitNewAbout, submitNewAboutSaga), takeLatest(types.submitNewUsername, submitNewUsernameSaga), takeLatest(types.validateUsername, validateUsernameSaga), - takeLatest(types.submitProfileUI, sumbitProfileUISaga) + takeLatest(types.submitProfileUI, sumbitProfileUISaga), + takeEvery(types.verifyCert, verifyCertificationSaga) ]; } diff --git a/client/src/resources/certProjectMap.js b/client/src/resources/certProjectMap.js index 5cbacf03ca..a9e1ffb6c0 100644 --- a/client/src/resources/certProjectMap.js +++ b/client/src/resources/certProjectMap.js @@ -16,162 +16,192 @@ export const projectMap = { { id: 'bd7158d8c442eddfaeb5bd18', title: 'Build a Tribute Page', - link: `${responsiveWebBase}/build-a-tribute-page` + link: `${responsiveWebBase}/build-a-tribute-page`, + superBlock: 'responsive-web-design' }, { id: '587d78af367417b2b2512b03', title: 'Build a Survey Form', - link: `${responsiveWebBase}/build-a-survey-form` + link: `${responsiveWebBase}/build-a-survey-form`, + superBlock: 'responsive-web-design' }, { id: '587d78af367417b2b2512b04', 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', 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', 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': [ { id: 'aaa48de84e1ecc7c742e1124', title: 'Palindrome Checker', - link: `${jsAlgoBase}/palindrome-checker` + link: `${jsAlgoBase}/palindrome-checker`, + superBlock: 'javascript-algorithms-and-data-structures' }, { id: 'a7f4d8f2483413a6ce226cac', title: 'Roman Numeral Converter', - link: `${jsAlgoBase}/roman-numeral-converter` + link: `${jsAlgoBase}/roman-numeral-converter`, + superBlock: 'javascript-algorithms-and-data-structures' }, { id: '56533eb9ac21ba0edf2244e2', title: 'Caesars Cipher', - link: `${jsAlgoBase}/caesars-cipher` + link: `${jsAlgoBase}/caesars-cipher`, + superBlock: 'javascript-algorithms-and-data-structures' }, { id: 'aff0395860f5d3034dc0bfc9', title: 'Telephone Number Validator', - link: `${jsAlgoBase}/telephone-number-validator` + link: `${jsAlgoBase}/telephone-number-validator`, + superBlock: 'javascript-algorithms-and-data-structures' }, { id: 'aa2e6f85cab2ab736c9a9b24', title: 'Cash Register', - link: `${jsAlgoBase}/cash-register` + link: `${jsAlgoBase}/cash-register`, + superBlock: 'javascript-algorithms-and-data-structures' } ], 'Front End Libraries': [ { id: 'bd7158d8c442eddfaeb5bd13', 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', title: 'Build a Markdown Previewer', - link: `${feLibsBase}/build-a-markdown-previewer` + link: `${feLibsBase}/build-a-markdown-previewer`, + superBlock: 'front-end-libraries' }, { id: '587d7dbc367417b2b2512bae', title: 'Build a Drum Machine', - link: `${feLibsBase}/build-a-drum-machine` + link: `${feLibsBase}/build-a-drum-machine`, + superBlock: 'front-end-libraries' }, { id: 'bd7158d8c442eddfaeb5bd17', title: 'Build a JavaScript Calculator', - link: `${feLibsBase}/build-a-javascript-calculator` + link: `${feLibsBase}/build-a-javascript-calculator`, + superBlock: 'front-end-libraries' }, { id: 'bd7158d8c442eddfaeb5bd0f', title: 'Build a Pomodoro Clock', - link: `${feLibsBase}/build-a-pomodoro-clock` + link: `${feLibsBase}/build-a-pomodoro-clock`, + superBlock: 'front-end-libraries' } ], 'Data Visualization': [ { id: 'bd7168d8c242eddfaeb5bd13', 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', 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', 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', 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', 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": [ { id: 'bd7158d8c443edefaeb5bdef', title: 'Timestamp Microservice', - link: `${apiMicroBase}/timestamp-microservice` + link: `${apiMicroBase}/timestamp-microservice`, + superBlock: 'apis-and-microservices' }, { id: 'bd7158d8c443edefaeb5bdff', title: 'Request Header Parser Microservice', - link: `${apiMicroBase}/request-header-parser-microservice` + link: `${apiMicroBase}/request-header-parser-microservice`, + superBlock: 'apis-and-microservices' }, { id: 'bd7158d8c443edefaeb5bd0e', title: 'URL Shortener Microservice', - link: `${apiMicroBase}/url-shortener-microservice` + link: `${apiMicroBase}/url-shortener-microservice`, + superBlock: 'apis-and-microservices' }, { id: '5a8b073d06fa14fcfde687aa', title: 'Exercise Tracker', - link: `${apiMicroBase}/exercise-tracker` + link: `${apiMicroBase}/exercise-tracker`, + superBlock: 'apis-and-microservices' }, { id: 'bd7158d8c443edefaeb5bd0f', title: 'File Metadata Microservice', - link: `${apiMicroBase}/file-metadata-microservice` + link: `${apiMicroBase}/file-metadata-microservice`, + superBlock: 'apis-and-microservices' } ], 'Information Security And Quality Assurance': [ { id: '587d8249367417b2b2512c41', title: 'Metric-Imperial Converter', - link: `${infoSecBase}/metric-imperial-converter` + link: `${infoSecBase}/metric-imperial-converter`, + superBlock: 'information-security-and-quality-assurance' }, { id: '587d8249367417b2b2512c42', title: 'Issue Tracker', - link: `${infoSecBase}/issue-tracker` + link: `${infoSecBase}/issue-tracker`, + superBlock: 'information-security-and-quality-assurance' }, { id: '587d824a367417b2b2512c43', title: 'Personal Library', - link: `${infoSecBase}/personal-library` + link: `${infoSecBase}/personal-library`, + superBlock: 'information-security-and-quality-assurance' }, { id: '587d824a367417b2b2512c44', title: 'Stock Price Checker', - link: `${infoSecBase}/stock-price-checker` + link: `${infoSecBase}/stock-price-checker`, + superBlock: 'information-security-and-quality-assurance' }, { id: '587d824a367417b2b2512c45', title: 'Anonymous Message Board', - link: `${infoSecBase}/anonymous-message-board` + link: `${infoSecBase}/anonymous-message-board`, + superBlock: 'information-security-and-quality-assurance' } ] }; diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 0d7b1a010c..a435a9f103 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -64,4 +64,8 @@ export function putUserUpdateEmail(email) { return put('/update-my-email', { email }); } +export function putVerifyCert(superBlock) { + return put('/certificate/verify', { superBlock }); +} + /** DELETE **/