feat(certs): Convert certification views to gatsby

This commit is contained in:
Bouncey
2018-09-04 14:54:23 +01:00
committed by mrugesh mohapatra
parent dc00eb8555
commit 77a4452437
30 changed files with 627 additions and 917 deletions

View File

@ -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,
{
return observeQuery(Challenge, 'findById', id, {
id: true,
tests: true,
name: true,
challengeType: true
}
)
.shareReplay();
}).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
}
};
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: {
updateData = {
...updateData,
completedChallenges: [
...user.completedChallenges,
{
id,
completedDate: new Date(),
challengeType
}
]
};
user.completedChallenges[
user.completedChallenges.length - 1
] = { id, completedDate: new Date() };
}
if (!user.name) {
@ -306,44 +254,41 @@ export default function certificate(app) {
// if not it noop
sendCertifiedEmail(user, Email.send$),
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
)
.map(
({ count, pledgeOrMessage }) => {
).map(({ count, pledgeOrMessage }) => {
if (typeof pledgeOrMessage === 'string') {
log(pledgeOrMessage);
}
log(`${count} documents updated`);
return successMessage(user.username, certName);
}
);
});
})
.subscribe(
(message) => {
.subscribe(message => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
});
},
next
);
}, 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,
{
const certTitle = certText[certType];
const completionTime = completionHours[certType] || 300;
return findUserByUsername$(username, {
isCheater: true,
isFrontEndCert: true,
isBackEndCert: true,
@ -360,91 +305,113 @@ export default function certificate(app) {
name: true,
completedChallenges: true,
profileUI: true
}
)
.subscribe(
user => {
}).subscribe(user => {
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
return res.json({
messages: [
{
type: 'info',
message: `We could not find a user with the username "${username}"`
}
]
});
}
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`;
if (!user.name) {
req.flash(
'danger',
dedent`
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);
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`
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`
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`
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
) || {};
const { completedDate = new Date() } =
_.find(completedChallenges, ({ id }) => certId === id) || {};
return res.render(
certViews[certType],
const { username, name } = user;
return res.json({
certTitle,
username,
name,
date: format(new Date(completedDate), 'MMMM D, YYYY'),
completionTime
});
}
return res.json({
messages: [
{
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
);
type: 'info',
message: `It looks like user ${username} is not ${
certText[certType]
} certified`
}
]
});
}, next);
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {

View File

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

View File

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

View File

@ -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 <RedirectHome />;
}
const { pending, complete, errored } = fetchState;
if (pending) {
return (
<div className='loader-wrapper'>
<Loader />
</div>
);
}
if (!pending && errored) {
createFlashMessage(standardErrorMessage);
return <RedirectHome />;
}
if (!pending && !complete && !errored) {
createFlashMessage(reallyWeirdErrorMessage);
return <RedirectHome />;
}
const {
date: issueDate,
name: userFullName,
username,
certTitle,
completionTime
} = cert;
return (
<Grid className='certificate-wrapper certification-namespace'>
<Row>
<header>
<Col md={5} sm={12}>
<div className='logo'>
<Image
alt="freeCodeCamp.org's Logo"
responsive={true}
src={
'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo' +
'.svg'
}
/>
</div>
</Col>
<Col md={7} sm={12}>
<div className='issue-date'>
Issued&nbsp;
<strong>{issueDate}</strong>
</div>
</Col>
</header>
<section className='information'>
<div className='information-container'>
<h3>This certifies that</h3>
<h1>
<strong>{userFullName}</strong>
</h1>
<h3>has successfully completed the freeCodeCamp.org</h3>
<h1>
<strong>{certTitle} Certification</strong>
</h1>
<h4>
Developer Certification, representing approximately{' '}
{completionTime} hours of coursework
</h4>
</div>
</section>
<footer>
<div className='row signatures'>
<Image
alt="Quincy Larson's Signature"
src='https://i.imgur.com/OJFVJKg.png'
/>
<p>
<strong>Quincy Larson</strong>
</p>
<p>Executive Director, freeCodeCamp.org</p>
</div>
<Row>
<p className='verify'>
Verify this certification at:
https://www.freecodecamp.org/certification/
{username}/{certName}
</p>
</Row>
</footer>
</Row>
</Grid>
);
}
}
ShowCertification.displayName = 'ShowCertification';
ShowCertification.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShowCertification);

View File

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

View File

@ -0,0 +1,8 @@
import { navigate } from 'gatsby';
const Redirecthome = () => {
navigate('/');
return null;
};
export default Redirecthome;

View File

@ -1,20 +1,9 @@
import React from 'react';
import Helmet from 'react-helmet';
import Spinner from 'react-spinkit';
function Loader() {
return (
<div className='full-size'>
<Helmet>
<link href='/css/loader.css' rel='stylesheet' />
</Helmet>
<div className='loader full-size'>
<div className='ball-scale-ripple-multiple'>
<div/>
<div/>
<div/>
</div>
</div>
</div>
<Spinner name='ball-clip-rotate-multiple'/>
);
}

View File

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

View File

@ -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 <h2>Certs</h2>;
return (
<Router>
<ShowCertification path='/certification/:username/:certName' />
<Redirecthome default={true} />
</Router>
);
}
}

View File

@ -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 initialState = {
appUsername: '',
fetchState: {
const defaultFetchState = {
pending: true,
complete: false,
errored: false,
error: null
};
const initialState = {
appUsername: '',
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,

View File

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

View File

@ -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 **/

View File

@ -0,0 +1,8 @@
export default {
type: 'danger',
message:
'Something really weird happened, if it happens again, please consider ' +
"raising an issue on <a href='https://github.com/freeCodeCamp" +
"/freeCodeCamp/issues/new' target='_blank' rel='nofollow " +
"noreferrer'>GitHub</a>."
};

View File

@ -0,0 +1,4 @@
export default {
type: 'danger',
message: 'Something went wrong, please check and try again'
};

View File

@ -5,3 +5,9 @@
/deprecated-signin /signin 301
/logout /signout 301
/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

View File

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