diff --git a/api-server/server/boot/certificate.js b/api-server/server/boot/certificate.js index 266bf9546c..6961366334 100644 --- a/api-server/server/boot/certificate.js +++ b/api-server/server/boot/certificate.js @@ -1,23 +1,18 @@ import _ from 'lodash'; import loopback from 'loopback'; -import moment from 'moment-timezone'; import path from 'path'; import dedent from 'dedent'; import { Observable } from 'rx'; import debug from 'debug'; import { isEmail } from 'validator'; +import format from 'date-fns/format'; -import { - ifNoUser401 -} from '../utils/middleware'; - +import { ifNoUser401 } from '../utils/middleware'; import { observeQuery } from '../utils/rx'; - import { legacyFrontEndChallengeId, legacyBackEndChallengeId, legacyDataVisId, - respWebDesignId, frontEndLibsId, jsAlgoDataStructId, @@ -28,26 +23,83 @@ import { } from '../utils/constantStrings.json'; import certTypes from '../utils/certTypes.json'; import superBlockCertTypeMap from '../utils/superBlockCertTypeMap'; -import { - completeCommitment$ -} from '../utils/commit'; +import { completeCommitment$ } from '../utils/commit'; const log = debug('fcc:certification'); -const renderCertifedEmail = loopback.template(path.join( - __dirname, - '..', - 'views', - 'emails', - 'certified.ejs' -)); + +export default function bootCertificate(app) { + const api = app.loopback.Router(); + + const certTypeIds = createCertTypeIds(app); + const showCert = createShowCert(app); + const verifyCert = createVerifyCert(certTypeIds, app); + + api.post('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert); + api.get('/certificate/showCert/:username/:cert', showCert); + + app.use('/internal', api); +} + +const noNameMessage = dedent` + We need your name so we can put it on your certification. + Add your name to your account settings and click the save button. + Then we can issue your certification. + `; + +const notCertifiedMessage = name => dedent` + it looks like you have not completed the necessary steps. + Please complete the required challenges to claim the + ${name} + `; + +const alreadyClaimedMessage = name => dedent` + It looks like you already have claimed the ${name} + `; + +const successMessage = (username, name) => dedent` + @${username}, you have successfully claimed + the ${name}! + Congratulations on behalf of the freeCodeCamp.org team! + `; + +function ifNoSuperBlock404(req, res, next) { + const { superBlock } = req.body; + if (superBlock && superBlocks.includes(superBlock)) { + return next(); + } + return res.status(404).end(); +} + +const renderCertifedEmail = loopback.template( + path.join(__dirname, '..', 'views', 'emails', 'certified.ejs') +); + +function createCertTypeIds(app) { + const { Challenge } = app.models; + + return { + // legacy + [certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge), + [certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge), + [certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge), + + // modern + [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), + [certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge), + [certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge), + [certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge), + [certTypes.apisMicroservices]: getIdsForCert$( + apisMicroservicesId, + Challenge + ), + [certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge), + [certTypes.fullStack]: getIdsForCert$(fullStackId, Challenge) + }; +} function isCertified(ids, completedChallenges = []) { - return _.every( - ids, - ({ id }) => _.find( - completedChallenges, - ({ id: completedId }) => completedId === id - ) + return _.every(ids, ({ id }) => + _.find(completedChallenges, ({ id: completedId }) => completedId === id) ); } @@ -64,51 +116,43 @@ const certIds = { [certTypes.fullStack]: fullStackId }; -const certViews = { - [certTypes.frontEnd]: 'certificate/legacy/front-end.jade', - [certTypes.backEnd]: 'certificate/legacy/back-end.jade', - [certTypes.dataVis]: 'certificate/legacy/data-visualization.jade', - - [certTypes.respWebDesign]: 'certificate/responsive-web-design.jade', - [certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade', - [certTypes.jsAlgoDataStruct]: - 'certificate/javascript-algorithms-and-data-structures.jade', - [certTypes.dataVis2018]: 'certificate/data-visualization.jade', - [certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade', - [certTypes.infosecQa]: - 'certificate/information-security-and-quality-assurance.jade', - [certTypes.fullStack]: 'certificate/full-stack.jade' -}; - const certText = { [certTypes.frontEnd]: 'Legacy Front End', [certTypes.backEnd]: 'Legacy Back End', [certTypes.dataVis]: 'Legacy Data Visualization', - [certTypes.fullStack]: 'Legacy Full Stack', + [certTypes.fullStack]: 'Full Stack', [certTypes.respWebDesign]: 'Responsive Web Design', [certTypes.frontEndLibs]: 'Front End Libraries', - [certTypes.jsAlgoDataStruct]: - 'JavaScript Algorithms and Data Structures', + [certTypes.jsAlgoDataStruct]: 'JavaScript Algorithms and Data Structures', [certTypes.dataVis2018]: 'Data Visualization', [certTypes.apisMicroservices]: 'APIs and Microservices', [certTypes.infosecQa]: 'Information Security and Quality Assurance' }; +const completionHours = { + [certTypes.frontEnd]: 400, + [certTypes.backEnd]: 400, + [certTypes.dataVis]: 400, + [certTypes.fullStack]: 1800, + [certTypes.respWebDesign]: 300, + [certTypes.frontEndLibs]: 300, + [certTypes.jsAlgoDataStruct]: 300, + [certTypes.dataVis2018]: 300, + [certTypes.apisMicroservices]: 300, + [certTypes.infosecQa]: 300 +}; + function getIdsForCert$(id, Challenge) { - return observeQuery( - Challenge, - 'findById', - id, - { - id: true, - tests: true, - name: true, - challengeType: true - } - ) - .shareReplay(); + return observeQuery(Challenge, 'findById', id, { + id: true, + tests: true, + name: true, + challengeType: true + }).shareReplay(); } +const superBlocks = Object.keys(superBlockCertTypeMap); + function sendCertifiedEmail( { email = '', @@ -150,111 +194,18 @@ function sendCertifiedEmail( return send$(notifyUser).map(() => true); } -export default function certificate(app) { - const router = app.loopback.Router(); - const { Email, Challenge, User } = app.models; - - function findUserByUsername$(username, fields) { - return observeQuery( - User, - 'findOne', - { - where: { username }, - fields - } - ); - } - - const certTypeIds = { - // legacy - [certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge), - [certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge), - [certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge), - - // modern - [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), - [certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge), - [certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge), - [certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge), - [certTypes.apisMicroservices]: getIdsForCert$( - apisMicroservicesId, - Challenge - ), - [certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge), - [certTypes.fullStack]: getIdsForCert$(fullStackId, Challenge) - }; - - const superBlocks = Object.keys(superBlockCertTypeMap); - - router.get( - '/:username/front-end-certification', - (req, res) => res.redirect( - `/certification/${req.params.username}/legacy-front-end` - ) - ); - - router.get( - '/:username/data-visualization-certification', - (req, res) => res.redirect( - `/certification/${req.params.username}/legacy-data-visualization` - ) - ); - - router.get( - '/:username/back-end-certification', - (req, res) => res.redirect( - `/certification/${req.params.username}/legacy-back-end` - ) - ); - - router.get( - '/:username/full-stack-certification', - (req, res) => res.redirect( - `/certification/${req.params.username}/full-stack` - ) - ); - - router.post( - '/certificate/verify', - ifNoUser401, - ifNoSuperBlock404, - verifyCert - ); - router.get( - '/certification/:username/:cert', - showCert - ); - - app.use(router); - - const noNameMessage = dedent` - We need your name so we can put it on your certification. - Add your name to your account settings and click the save button. - Then we can issue your certification. - `; - - const notCertifiedMessage = name => dedent` - it looks like you have not completed the necessary steps. - Please complete the required challenges to claim the - ${name} - `; - - const alreadyClaimedMessage = name => dedent` - It looks like you already have claimed the ${name} - `; - - const successMessage = (username, name) => dedent` - @${username}, you have successfully claimed - the ${name}! - Congratulations on behalf of the freeCodeCamp team! - `; - - function verifyCert(req, res, next) { - const { body: { superBlock }, user } = req; +function createVerifyCert(certTypeIds, app) { + const { Email } = app.models; + return function verifyCert(req, res, next) { + const { + body: { superBlock }, + user + } = req; log(superBlock); let certType = superBlockCertTypeMap[superBlock]; log(certType); - return user.getCompletedChallenges$() + return user + .getCompletedChallenges$() .flatMap(() => certTypeIds[certType]) .flatMap(challenge => { const certName = certText[certType]; @@ -263,31 +214,28 @@ export default function certificate(app) { } let updateData = { - $set: { - [certType]: true - } + [certType]: true }; if (challenge) { - const { - id, - tests, - challengeType - } = challenge; - if (!user[certType] && - !isCertified(tests, user.completedChallenges)) { + const { id, tests, challengeType } = challenge; + if ( + !user[certType] && + !isCertified(tests, user.completedChallenges) + ) { return Observable.just(notCertifiedMessage(certName)); } - updateData['$push'] = { - completedChallenges: { - id, - completedDate: new Date(), - challengeType - } + updateData = { + ...updateData, + completedChallenges: [ + ...user.completedChallenges, + { + id, + completedDate: new Date(), + challengeType + } + ] }; - user.completedChallenges[ - user.completedChallenges.length - 1 - ] = { id, completedDate: new Date() }; } if (!user.name) { @@ -306,145 +254,164 @@ export default function certificate(app) { // if not it noop sendCertifiedEmail(user, Email.send$), ({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage }) - ) - .map( - ({ count, pledgeOrMessage }) => { - if (typeof pledgeOrMessage === 'string') { - log(pledgeOrMessage); - } - log(`${count} documents updated`); - return successMessage(user.username, certName); - } - ); - }) - .subscribe( - (message) => { - return res.status(200).json({ - message, - success: message.includes('Congratulations') - }); - }, - next - ); + ).map(({ count, pledgeOrMessage }) => { + if (typeof pledgeOrMessage === 'string') { + log(pledgeOrMessage); + } + log(`${count} documents updated`); + return successMessage(user.username, certName); + }); + }) + .subscribe(message => { + return res.status(200).json({ + message, + success: message.includes('Congratulations') + }); + }, next); + }; +} + +function createShowCert(app) { + const { User } = app.models; + + function findUserByUsername$(username, fields) { + return observeQuery(User, 'findOne', { + where: { username }, + fields + }); } - function ifNoSuperBlock404(req, res, next) { - const { superBlock } = req.body; - if (superBlock && superBlocks.includes(superBlock)) { - return next(); - } - return res.status(404).end(); - } - - function showCert(req, res, next) { + return function showCert(req, res, next) { let { username, cert } = req.params; username = username.toLowerCase(); const certType = superBlockCertTypeMap[cert]; const certId = certIds[certType]; - return findUserByUsername$( - username, - { - isCheater: true, - isFrontEndCert: true, - isBackEndCert: true, - isFullStackCert: true, - isRespWebDesignCert: true, - isFrontEndLibsCert: true, - isJsAlgoDataStructCert: true, - isDataVisCert: true, - is2018DataVisCert: true, - isApisMicroservicesCert: true, - isInfosecQaCert: true, - isHonest: true, - username: true, - name: true, - completedChallenges: true, - profileUI: true + const certTitle = certText[certType]; + const completionTime = completionHours[certType] || 300; + return findUserByUsername$(username, { + isCheater: true, + isFrontEndCert: true, + isBackEndCert: true, + isFullStackCert: true, + isRespWebDesignCert: true, + isFrontEndLibsCert: true, + isJsAlgoDataStructCert: true, + isDataVisCert: true, + is2018DataVisCert: true, + isApisMicroservicesCert: true, + isInfosecQaCert: true, + isHonest: true, + username: true, + name: true, + completedChallenges: true, + profileUI: true + }).subscribe(user => { + if (!user) { + return res.json({ + messages: [ + { + type: 'info', + message: `We could not find a user with the username "${username}"` + } + ] + }); } - ) - .subscribe( - user => { - if (!user) { - req.flash( - 'danger', - `We couldn't find a user with the username ${username}` - ); - return res.redirect('/'); - } - const { isLocked, showCerts } = user.profileUI; - const profile = `/portfolio/${user.username}`; + const { isLocked, showCerts } = user.profileUI; - if (!user.name) { - req.flash( - 'danger', - dedent` + if (!user.name) { + return res.json({ + messages: [ + { + type: 'info', + message: dedent` This user needs to add their name to their account in order for others to be able to view their certification. ` - ); - return res.redirect(profile); - } + } + ] + }); + } - if (user.isCheater) { - return res.redirect(profile); - } + if (user.isCheater) { + return res.json({ + messages: [ + { + type: 'info', + message: + 'This user is not eligible for freeCodeCamp.org ' + + 'certifications at this time' + } + ] + }); + } - if (isLocked) { - req.flash( - 'danger', - dedent` + if (isLocked) { + return res.json({ + messages: [ + { + type: 'info', + message: dedent` ${username} has chosen to make their profile private. They will need to make their profile public in order for others to be able to view their certification. ` - ); - return res.redirect('/'); - } + } + ] + }); + } - if (!showCerts) { - req.flash( - 'danger', - dedent` + if (!showCerts) { + return res.json({ + messages: [ + { + type: 'info', + message: dedent` ${username} has chosen to make their certifications private. They will need to make their certifications public in order for others to be able to view them. ` - ); - return res.redirect('/'); - } + } + ] + }); + } - if (!user.isHonest) { - req.flash( - 'danger', - dedent` + if (!user.isHonest) { + return res.json({ + messages: [ + { + type: 'info', + message: dedent` ${username} has not yet agreed to our Academic Honesty Pledge. ` - ); - return res.redirect(profile); - } - - if (user[certType]) { - const { completedChallenges = [] } = user; - const { completedDate = new Date() } = _.find( - completedChallenges, ({ id }) => certId === id - ) || {}; - - return res.render( - certViews[certType], - { - username: user.username, - date: moment(new Date(completedDate)).format('MMMM D, YYYY'), - name: user.name } - ); - } - req.flash( - 'danger', - `Looks like user ${username} is not ${certText[certType]} certified` - ); - return res.redirect(profile); - }, - next - ); - } + ] + }); + } + + if (user[certType]) { + const { completedChallenges = [] } = user; + const { completedDate = new Date() } = + _.find(completedChallenges, ({ id }) => certId === id) || {}; + + const { username, name } = user; + return res.json({ + certTitle, + username, + name, + date: format(new Date(completedDate), 'MMMM D, YYYY'), + completionTime + }); + } + return res.json({ + messages: [ + { + type: 'info', + message: `It looks like user ${username} is not ${ + certText[certType] + } certified` + } + ] + }); + }, next); + }; } diff --git a/api-server/server/views/certificate/advanced-front-end.jade b/api-server/server/views/certificate/advanced-front-end.jade deleted file mode 100644 index 497e4e62e9..0000000000 --- a/api-server/server/views/certificate/advanced-front-end.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Advanced Frontend - h4 1 of 3 legacy freeCodeCamp certification, representing approximately 400 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/advanced-front-end \ No newline at end of file diff --git a/api-server/server/views/certificate/apis-and-microservices.jade b/api-server/server/views/certificate/apis-and-microservices.jade deleted file mode 100644 index c4c5fcfb8b..0000000000 --- a/api-server/server/views/certificate/apis-and-microservices.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong APIs and Microservices - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/apis-and-microservices diff --git a/api-server/server/views/certificate/data-visualization.jade b/api-server/server/views/certificate/data-visualization.jade deleted file mode 100644 index 174dc082f5..0000000000 --- a/api-server/server/views/certificate/data-visualization.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Data Visualization - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/data-visualization diff --git a/api-server/server/views/certificate/front-end-libraries.jade b/api-server/server/views/certificate/front-end-libraries.jade deleted file mode 100644 index dbb52f6c9c..0000000000 --- a/api-server/server/views/certificate/front-end-libraries.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Front End Libraries - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/front-end-libraries diff --git a/api-server/server/views/certificate/full-stack.jade b/api-server/server/views/certificate/full-stack.jade deleted file mode 100644 index 1cd0214ba5..0000000000 --- a/api-server/server/views/certificate/full-stack.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Full Stack - h4 Developer Certification, representing approximately 1800 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/responsive-web-design diff --git a/api-server/server/views/certificate/information-security-and-quality-assurance.jade b/api-server/server/views/certificate/information-security-and-quality-assurance.jade deleted file mode 100644 index 436dbb2ac5..0000000000 --- a/api-server/server/views/certificate/information-security-and-quality-assurance.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Information Security and Quality Assurance - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/information-security-and-quality-assurance diff --git a/api-server/server/views/certificate/javascript-algorithms-and-data-structures.jade b/api-server/server/views/certificate/javascript-algorithms-and-data-structures.jade deleted file mode 100644 index 2892a554df..0000000000 --- a/api-server/server/views/certificate/javascript-algorithms-and-data-structures.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong JavaScript Algorithms and Data Structures - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/javascript-algorithms-and-data-structures diff --git a/api-server/server/views/certificate/legacy/back-end.jade b/api-server/server/views/certificate/legacy/back-end.jade deleted file mode 100644 index f1daeb439a..0000000000 --- a/api-server/server/views/certificate/legacy/back-end.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include ../styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Back End Development - h4 Certification, representing approximately 400 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/legacy-back-end diff --git a/api-server/server/views/certificate/legacy/data-visualization.jade b/api-server/server/views/certificate/legacy/data-visualization.jade deleted file mode 100644 index f3ae4c76ce..0000000000 --- a/api-server/server/views/certificate/legacy/data-visualization.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include ../styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Data Visualization Projects - h4 Developer Certification, representing approximately 400 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/legacy-data-visualization diff --git a/api-server/server/views/certificate/legacy/front-end.jade b/api-server/server/views/certificate/legacy/front-end.jade deleted file mode 100644 index 53382cd72e..0000000000 --- a/api-server/server/views/certificate/legacy/front-end.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include ../styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Front End Development - h4 Certification, representing approximately 400 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/legacy-front-end diff --git a/api-server/server/views/certificate/legacy/full-stack.jade b/api-server/server/views/certificate/legacy/full-stack.jade deleted file mode 100644 index 92c840cd9c..0000000000 --- a/api-server/server/views/certificate/legacy/full-stack.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include ../styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Legacy Full Stack Development Program - h4 All three of the legacy freeCodeCamp certifications, representing approximately 1,200 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/legacy-full-stack \ No newline at end of file diff --git a/api-server/server/views/certificate/responsive-web-design.jade b/api-server/server/views/certificate/responsive-web-design.jade deleted file mode 100644 index 55dd9f27e2..0000000000 --- a/api-server/server/views/certificate/responsive-web-design.jade +++ /dev/null @@ -1,32 +0,0 @@ -meta(name='viewport', content='width=device-width, initial-scale=1') -link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css') -include styles - -.certificate-wrapper.container - .row - header - .col-md-5.col-sm-12 - .logo - img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo") - .col-md-7.col-sm-12 - .issue-date Issued  - strong #{date} - - section.information - .information-container - h3 This certifies that - h1 - strong= name - h3 has successfully completed freeCodeCamp's - h1 - strong Responsive Web Design - h4 Developer Certification, representing approximately 300 hours of coursework - - footer - .row.signatures - img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature") - p - strong Quincy Larson - p Executive Director, freeCodeCamp.org - .row - p.verify Verify this certification at: https://www.freecodecamp.org/certification/#{username}/responsive-web-design diff --git a/api-server/server/views/certificate/styles.jade b/api-server/server/views/certificate/styles.jade deleted file mode 100644 index 0e3c866bc5..0000000000 --- a/api-server/server/views/certificate/styles.jade +++ /dev/null @@ -1,203 +0,0 @@ -style. - @font-face { - font-family: "Sax Mono"; - src: url("/fonts/saxmono.ttf") format("truetype"); - } - - * { - margin: 0; - padding: 0; - } - - .container { - max-width: 1500px; - width: 100%; - padding: 30px; - border: darkgreen 15px solid; - border-radius: 3px; - } - - .row { - margin: 0; - } - - .col-sm-12 { - padding: 0; - } - - .certificate-wrapper { - top: 0; - position: relative; - font-family: "Sax Mono", monospace; - } - - header { - width: 100%; - height: 140px; - /*background-image: url('https://i.imgur.com/1FK6aaN.png'); - background-repeat: no-repeat; - background-position: top; - background-size: cover;*/ - background-color: darkgreen; - } - - .logo { - display: flex; - align-items: center; - height: 140px; - margin-left: 100px; - } - - .logo img { - max-width: 500px; - width: 100%; - } - - .issue-date { - line-height: 140px; - font-size: 20px; - text-align: right; - margin-right: 100px; - color: #fff; - } - - .information { - margin-top: -20px; - height: 380px; - text-align: center; - background-color: #efefef; - } - - .information-container { - position: relative; - top: 50%; - transform: translateY(-50%); - margin: 0px 100px; - } - - p { - margin: 0; - } - - h3 { - font-size: 30px; - } - - h4 { - margin-top: 25px; - font-size: 20px; - } - - h1 { - font-size: 40px; - color: #006400; - } - - .signatures { - text-align: center; - margin: 0 auto; - background-color: #efefef; - } - - .signatures img { - max-width: 300px; - width: 100%; - margin: 0 auto; - } - - .signatures p { - font-size: 18px; - padding-top: 10px; - } - - .verify { - padding: 30px 0; - font-size: 15px; - text-align: center; - word-wrap: break-word; - background-color: #efefef; - } - - /*adds vertical alignment when the height is bigger than required to display everything*/ - @media screen and (min-height: 700px) { - body { - display: flex; - flex-direction: column; - justify-content: center; - height: 100vh; - } - } - - /*mobile media queries*/ - @media screen and (max-width: 992px) { - header { - height: 160px; - } - - .logo { - margin-left: 0; - padding: 20px; - justify-content: center; - height: 80px; - } - - .logo img { - margin-top: 20px; - } - - .issue-date { - margin-top: 10px; - margin-right: 0; - text-align: center; - line-height: 0px; - word-wrap: break-word; - } - - .issue-date strong { - display: block; - margin-top: 15px; - line-height: 25px; - } - - .information { - height: 300px; - } - - .information-container { - margin: 0px 15px; - text-align: center; - word-wrap: break-word; - } - - h3 { - font-size: 25px; - } - - h1 { - font-size: 28px; - } - } - - @media screen and (max-width: 675px) { - .container { - padding: 0; - border: 0; - } - - header { - height: 190px; - } - - h3 { - font-size: 15px; - } - - h1 { - font-size: 17px; - } - - h4 { - font-size: 15px; - margin-top: 20px; - } - } diff --git a/client/gatsby-config.js b/client/gatsby-config.js index 4ee0192d26..6e0a7100ff 100644 --- a/client/gatsby-config.js +++ b/client/gatsby-config.js @@ -8,6 +8,10 @@ module.exports = { }, plugins: [ 'gatsby-plugin-react-helmet', + { + resolve: 'gatsby-plugin-create-client-paths', + options: { prefixes: ['/certification/*'] } + }, { resolve: 'gatsby-plugin-manifest', options: { diff --git a/client/package-lock.json b/client/package-lock.json index 95da821cb1..fcc1519418 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -5954,6 +5954,14 @@ } } }, + "gatsby-plugin-create-client-paths": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/gatsby-plugin-create-client-paths/-/gatsby-plugin-create-client-paths-2.0.0-rc.1.tgz", + "integrity": "sha512-rWnqFww3pcEiGLg5d/5l/mS1GA13VhN/5Y8p7TC/5Ff9owaZd4nWeNdtyImdFnWZNROFEJwkLsB8pFJtzyEPuw==", + "requires": { + "@babel/runtime": "7.0.0" + } + }, "gatsby-plugin-manifest": { "version": "2.0.2-rc.1", "resolved": "https://registry.npmjs.org/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.0.2-rc.1.tgz", diff --git a/client/package.json b/client/package.json index 69f16396fe..a016af283f 100644 --- a/client/package.json +++ b/client/package.json @@ -10,9 +10,11 @@ "@fortawesome/free-regular-svg-icons": "^5.2.0", "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "0.0.20", + "@reach/router": "^1.1.1", "axios": "^0.18.0", "gatsby": "^2.0.0-rc.1", "gatsby-link": "^2.0.0-rc.1", + "gatsby-plugin-create-client-paths": "^2.0.0-rc.1", "gatsby-plugin-manifest": "next", "gatsby-plugin-react-helmet": "next", "gatsby-plugin-sitemap": "^2.0.0-rc.1", diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js new file mode 100644 index 0000000000..ee8adf877f --- /dev/null +++ b/client/src/client-only-routes/ShowCertification.js @@ -0,0 +1,182 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Grid, Row, Col, Image } from 'react-bootstrap'; + +import { + showCertSelector, + showCertFetchStateSelector, + showCert +} from '../redux'; +import validCertNames from '../../utils/validCertNames'; +import { createFlashMessage } from '../components/Flash/redux'; +import standardErrorMessage from '../utils/standardErrorMessage'; +import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage'; + +import RedirectHome from '../components/RedirectHome'; +import { Loader } from '../components/helpers'; + +import './show-certification.css'; + +const propTypes = { + cert: PropTypes.shape({ + username: PropTypes.string, + name: PropTypes.string, + certName: PropTypes.string, + date: PropTypes.string + }), + certDashedName: PropTypes.string, + certName: PropTypes.string, + certTitle: PropTypes.string, + createFlashMessage: PropTypes.func.isRequired, + fetchState: PropTypes.shape({ + pending: PropTypes.bool, + complete: PropTypes.bool, + errored: PropTypes.bool + }), + issueDate: PropTypes.string, + showCert: PropTypes.func.isRequired, + userFullName: PropTypes.string, + username: PropTypes.string, + validCertName: PropTypes.bool +}; + +const mapStateToProps = (state, { certName }) => { + const validCertName = validCertNames.some(name => name === certName); + return createSelector( + showCertSelector, + showCertFetchStateSelector, + (cert, fetchState) => ({ + cert, + fetchState, + validCertName + }) + ); +}; + +const mapDispatchToProps = dispatch => + bindActionCreators({ createFlashMessage, showCert }, dispatch); + +class ShowCertification extends Component { + componentDidMount() { + const { username, certName, validCertName, showCert } = this.props; + if (validCertName) { + return showCert({ username, certName }); + } + return null; + } + render() { + const { + cert, + fetchState, + validCertName, + createFlashMessage, + certName + } = this.props; + + if (!validCertName) { + createFlashMessage(standardErrorMessage); + return ; + } + + const { pending, complete, errored } = fetchState; + + if (pending) { + return ( +
+ +
+ ); + } + + if (!pending && errored) { + createFlashMessage(standardErrorMessage); + return ; + } + + if (!pending && !complete && !errored) { + createFlashMessage(reallyWeirdErrorMessage); + return ; + } + + const { + date: issueDate, + name: userFullName, + username, + certTitle, + completionTime + } = cert; + return ( + + +
+ +
+ freeCodeCamp.org's Logo +
+ + +
+ Issued  + {issueDate} +
+ +
+ +
+
+

This certifies that

+

+ {userFullName} +

+

has successfully completed the freeCodeCamp.org

+

+ {certTitle} Certification +

+

+ Developer Certification, representing approximately{' '} + {completionTime} hours of coursework +

+
+
+
+
+ Quincy Larson's Signature +

+ Quincy Larson +

+

Executive Director, freeCodeCamp.org

+
+ +

+ Verify this certification at: + https://www.freecodecamp.org/certification/ + {username}/{certName} +

+
+
+
+
+ ); + } +} + +ShowCertification.displayName = 'ShowCertification'; +ShowCertification.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShowCertification); diff --git a/client/src/client-only-routes/show-certification.css b/client/src/client-only-routes/show-certification.css new file mode 100644 index 0000000000..d7678275ba --- /dev/null +++ b/client/src/client-only-routes/show-certification.css @@ -0,0 +1,12 @@ +.loader-wrapper { + height: 100vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.loader-wrapper .sk-spinner { + transform: scale(8); + color: #006400; +} \ No newline at end of file diff --git a/client/src/components/RedirectHome.js b/client/src/components/RedirectHome.js new file mode 100644 index 0000000000..54503dd14e --- /dev/null +++ b/client/src/components/RedirectHome.js @@ -0,0 +1,8 @@ +import { navigate } from 'gatsby'; + +const Redirecthome = () => { + navigate('/'); + return null; +}; + +export default Redirecthome; diff --git a/client/src/components/helpers/Loader.js b/client/src/components/helpers/Loader.js index 07889fe4e6..77256c1fd9 100644 --- a/client/src/components/helpers/Loader.js +++ b/client/src/components/helpers/Loader.js @@ -1,20 +1,9 @@ import React from 'react'; -import Helmet from 'react-helmet'; +import Spinner from 'react-spinkit'; function Loader() { return ( -
- - - -
-
-
-
-
-
-
-
+ ); } diff --git a/client/src/pages/certification.css b/client/src/pages/certification.css index 71f86d713c..7315a04a42 100644 --- a/client/src/pages/certification.css +++ b/client/src/pages/certification.css @@ -7,8 +7,10 @@ margin: 0; padding: 0; } - -.certification-namespace .container { +.certification-namespace h1 { + margin: 12px 0; +} +.certification-namespace.container { max-width: 1500px; width: 100%; padding: 30px; @@ -24,8 +26,8 @@ padding: 0; } -.certification-namespace .certificate-wrapper { - top: 0; +.certification-namespace.certificate-wrapper { + top: calc(100vh / 20); position: relative; font-family: 'Sax Mono', monospace; } @@ -33,11 +35,8 @@ .certification-namespace header { width: 100%; height: 140px; - /*background-image: url('https://i.imgur.com/1FK6aaN.png'); - background-repeat: no-repeat; - background-position: top; - background-size: cover;*/ background-color: darkgreen; + position: relative; } .certification-namespace .logo { @@ -178,7 +177,7 @@ } @media screen and (max-width: 675px) { - .certification-namespace .container { + .certification-namespaces.issue-date { padding: 0; border: 0; } diff --git a/client/src/pages/certification.js b/client/src/pages/certification.js index c16ea8dbc8..a638817f40 100644 --- a/client/src/pages/certification.js +++ b/client/src/pages/certification.js @@ -1,10 +1,20 @@ import React, { Component } from 'react'; +import { Router } from '@reach/router'; + +import Redirecthome from '../components/RedirectHome'; +import ShowCertification from '../client-only-routes/ShowCertification'; import './certification.css'; + class Certification extends Component { render() { - return

Certs

; + return ( + + + + + ); } } diff --git a/client/src/redux/index.js b/client/src/redux/index.js index e03baa6840..675503b781 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -4,19 +4,28 @@ import { createTypes, createAsyncTypes } from '../utils/createTypes'; import { createFetchUserSaga } from './fetch-user-saga'; import { createAcceptTermsSaga } from './accept-terms-saga'; import { createAppMountSaga } from './app-mount-saga'; +import { createShowCertSaga } from './show-cert-saga'; import { createUpdateMyEmailSaga } from './update-email-saga'; const ns = 'app'; +const defaultFetchState = { + pending: true, + complete: false, + errored: false, + error: null +}; + const initialState = { appUsername: '', - fetchState: { - pending: true, - complete: false, - errored: false, - error: null + showCert: {}, + showCertFetchState: { + ...defaultFetchState }, - user: {} + user: {}, + userFetchState: { + ...defaultFetchState + } }; const types = createTypes( @@ -24,6 +33,7 @@ const types = createTypes( 'appMount', ...createAsyncTypes('fetchUser'), ...createAsyncTypes('acceptTerms'), + ...createAsyncTypes('showCert'), ...createAsyncTypes('updateMyEmail') ], ns @@ -33,7 +43,8 @@ export const sagas = [ ...createAcceptTermsSaga(types), ...createAppMountSaga(types), ...createFetchUserSaga(types), - ...createUpdateMyEmailSaga(types) + ...createUpdateMyEmailSaga(types), + ...createShowCertSaga(types) ]; export const appMount = createAction(types.appMount); @@ -46,12 +57,24 @@ export const fetchUser = createAction(types.fetchUser); export const fetchUserComplete = createAction(types.fetchUserComplete); export const fetchUserError = createAction(types.fetchUserError); +export const showCert = createAction(types.showCert); +export const showCertComplete = createAction(types.showCertComplete); +export const showCertError = createAction(types.showCertError); + export const updateMyEmail = createAction(types.updateMyEmail); export const updateMyEmailComplete = createAction(types.updateMyEmailComplete); export const updateMyEmailError = createAction(types.updateMyEmailError); export const isSignedInSelector = state => !!Object.keys(state[ns].user).length; -export const userFetchStateSelector = state => state[ns].fetchState; + +export const showCertSelector = state => state[ns].showCert; +export const showCertFetchStateSelector = state => state[ns].showCertFetchState; + +export const userByNameSelector = username => state => { + const { user } = state[ns]; + return username in user ? user[username] : {}; +}; +export const userFetchStateSelector = state => state[ns].userFetchState; export const usernameSelector = state => state[ns].appUsername; export const userSelector = state => state[ns].user; @@ -59,18 +82,13 @@ export const reducer = handleActions( { [types.fetchUser]: state => ({ ...state, - fetchState: { - pending: true, - complete: false, - errored: false, - error: null - } + userFetchState: { ...defaultFetchState } }), [types.fetchUserComplete]: (state, { payload: { user, username } }) => ({ ...state, user, appUsername: username, - fetchState: { + userFetchState: { pending: false, complete: true, errored: false, @@ -79,7 +97,32 @@ export const reducer = handleActions( }), [types.fetchUserError]: (state, { payload }) => ({ ...state, - fetchState: { + userFetchState: { + pending: false, + complete: false, + errored: true, + error: payload + } + }), + [types.showCert]: state => ({ + ...state, + showCert: {}, + showCertFetchState: { ...defaultFetchState } + }), + [types.showCertComplete]: (state, { payload }) => ({ + ...state, + showCert: payload, + showCertFetchState: { + pending: false, + complete: true, + errored: false, + error: null + } + }), + [types.showCertError]: (state, { payload }) => ({ + ...state, + showCert: {}, + showCertFetchState: { pending: false, complete: false, errored: true, diff --git a/client/src/redux/show-cert-saga.js b/client/src/redux/show-cert-saga.js new file mode 100644 index 0000000000..2918d19f3f --- /dev/null +++ b/client/src/redux/show-cert-saga.js @@ -0,0 +1,27 @@ +import { put, takeEvery, call } from 'redux-saga/effects'; +import { navigateTo } from 'gatsby'; + +import { createFlashMessage } from '../components/Flash/redux'; +import { getShowCert } from '../utils/ajax'; +import { showCertComplete, showCertError } from '.'; + +function* getShowCertSaga({ payload: { username, certName: cert } }) { + try { + const { data: response } = yield call(getShowCert, username, cert); + const { messages } = response; + if (messages && messages.length) { + for (let i = 0; i < messages.length; i++) { + yield put(createFlashMessage(messages[i])); + } + yield call(navigateTo, '/'); + return; + } + yield put(showCertComplete(response)); + } catch (e) { + yield put(showCertError(e)); + } +} + +export function createShowCertSaga(types) { + return [takeEvery(types.showCert, getShowCertSaga)]; +} diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 55376bd514..927df75cd9 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -14,10 +14,24 @@ function put(path, body) { return axios.put(`${base}${path}`, body); } +// function del(path) { +// return axios.delete(`${base}${path}`); +// } + +/** GET **/ + export function getSessionUser() { return get('/user/get-session-user'); } +export function getShowCert(username, cert) { + return get(`/certificate/showCert/${username}/${cert}`); +} + +/** POST **/ + +/** PUT **/ + export function putUserAcceptsTerms(quincyEmails) { return put('/update-privacy-terms', { quincyEmails }); } @@ -25,3 +39,5 @@ export function putUserAcceptsTerms(quincyEmails) { export function putUserUpdateEmail(email) { return put('/update-my-email', { email }); } + +/** DELETE **/ diff --git a/client/src/utils/reallyWeirdErrorMessage.js b/client/src/utils/reallyWeirdErrorMessage.js new file mode 100644 index 0000000000..2ff5ae4bb1 --- /dev/null +++ b/client/src/utils/reallyWeirdErrorMessage.js @@ -0,0 +1,8 @@ +export default { + type: 'danger', + message: + 'Something really weird happened, if it happens again, please consider ' + + "raising an issue on GitHub." +}; diff --git a/client/src/utils/standardErrorMessage.js b/client/src/utils/standardErrorMessage.js new file mode 100644 index 0000000000..cc70476b2c --- /dev/null +++ b/client/src/utils/standardErrorMessage.js @@ -0,0 +1,4 @@ +export default { + type: 'danger', + message: 'Something went wrong, please check and try again' +}; diff --git a/client/static/_redirects b/client/static/_redirects index 9c76ea9fb0..2e370cc06f 100644 --- a/client/static/_redirects +++ b/client/static/_redirects @@ -4,4 +4,10 @@ /login /signin 301 /deprecated-signin /signin 301 /logout /signout 301 -/passwordless-change /confirm-email 301 \ No newline at end of file +/passwordless-change /confirm-email 301 + +# certification redirects +/:username/front-end-certification /certification/:username/legacy-front-end 301 +/:username/data-visualization-certification /certification/:username/legacy-data-visualization 301 +/:username/back-end-certification /certification/:username/legacy-back-end 301 +/:username/full-stack-certificatio /certification/:username/full-stack 301 \ No newline at end of file diff --git a/client/utils/validCertNames.js b/client/utils/validCertNames.js new file mode 100644 index 0000000000..967b6a62d7 --- /dev/null +++ b/client/utils/validCertNames.js @@ -0,0 +1,12 @@ +export default [ + 'responsive-web-design', + 'javascript-algorithms-and-data-structures', + 'front-end-libraries', + 'data-visualization', + 'apis-and-microservices', + 'information-security-and-quality-assurance', + 'full-stack', + 'legacy-front-end', + 'legacy-back-end', + 'legacy-data-visualization' +];