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