feat(settings): Expand Settings page functionality (#16664)

* fix(layout): Fix Settings layout in firefox

* chore(availableForHire): Remove available for hire setting

* feat(helpers): Use helper components for Settings layout

* fix(map): Fix undefined lang requested

* feat(settings): Expand Settings page functionality

* chore(pledge): Remove pledge from Settings

* fix(about): Adjust AboutSettings layout

* fix(portfolio): Improve PortfolioSettings layout

* fix(email): Improve EmailSettings layout

* fix(settings): Align save buttons with form fields

* fix(AHP): Format AHP

* fix(DangerZone): Adjust DangerZone layout

* fix(projectSettings): Change Button Copy

* fix(CertSettings): Fix certificate claim logic

* chore(lint): Lint
This commit is contained in:
Stuart Taylor
2018-02-16 23:18:53 +00:00
committed by Quincy Larson
parent 9f034f4f79
commit 24ef69cf7a
78 changed files with 4395 additions and 1724 deletions

View File

@@ -7,9 +7,11 @@ import { check } from 'express-validator/check';
import {
ifUserRedirectTo,
ifNoUserRedirectTo,
createValidatorErrorHandler
} from '../utils/middleware';
import { wrapHandledError } from '../utils/create-handled-error.js';
import { homeURL } from '../../common/utils/constantStrings.json';
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
// const debug = debugFactory('fcc:boot:auth');
@@ -22,6 +24,7 @@ module.exports = function enableAuthentication(app) {
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
app.enableAuth();
const ifUserRedirect = ifUserRedirectTo();
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeURL);
const router = app.loopback.Router();
const api = app.loopback.Router();
const { AuthToken, User } = app.models;
@@ -79,7 +82,8 @@ module.exports = function enableAuthentication(app) {
const {
query: {
email: encodedEmail,
token: authTokenId
token: authTokenId,
emailChange
} = {}
} = req;
@@ -122,14 +126,16 @@ module.exports = function enableAuthentication(app) {
);
}
if (user.email !== email) {
throw wrapHandledError(
new Error('user email does not match'),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
if (!emailChange || (emailChange && user.newEmail !== email)) {
throw wrapHandledError(
new Error('user email does not match'),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
}
}
return authToken.validate$()
.map(isValid => {
@@ -185,6 +191,13 @@ module.exports = function enableAuthentication(app) {
getPasswordlessAuth
);
router.get(
'/passwordless-change',
ifNoUserRedirectHome,
passwordlessGetValidators,
getPasswordlessAuth
);
const passwordlessPostValidators = [
check('email')
.isEmail()

View File

@@ -7,28 +7,31 @@ import debug from 'debug';
import { isEmail } from 'validator';
import {
ifNoUser401,
ifNoUserSend
ifNoUser401
} from '../utils/middleware';
import { observeQuery } from '../utils/rx';
import {
// legacy
frontEndChallengeId,
backEndChallengeId,
dataVisId,
// modern
respWebDesignId,
frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId,
frontEndChallengeId,
dataVisId,
apisMicroservicesId,
backEndChallengeId,
infosecQaId
} from '../utils/constantStrings.json';
import {
completeCommitment$
} from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
@@ -38,12 +41,9 @@ const renderCertifedEmail = loopback.template(path.join(
'emails',
'certified.ejs'
));
const sendMessageToNonUser = ifNoUserSend(
'must be logged in to complete.'
);
function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => challengeMap[id]);
return _.every(ids, ({ id }) => _.has(challengeMap, id));
}
function getIdsForCert$(id, Challenge) {
@@ -120,12 +120,16 @@ export default function certificate(app) {
const { Email, Challenge } = app.models;
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
[certTypes.dataVis2018]: getIdsForCert$(dataVis2018Id, Challenge),
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.apisMicroservices]: getIdsForCert$(
apisMicroservicesId,
Challenge
@@ -133,78 +137,65 @@ export default function certificate(app) {
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
};
router.post(
'/certificate/verify/front-end',
ifNoUser401,
verifyCert.bind(null, certTypes.frontEnd)
);
const superBlocks = Object.keys(superBlockCertTypeMap);
router.post(
'/certificate/verify/back-end',
'/certificate/verify',
ifNoUser401,
verifyCert.bind(null, certTypes.backEnd)
);
router.post(
'/certificate/verify/responsive-web-design',
ifNoUser401,
verifyCert.bind(null, certTypes.respWebDesign)
);
router.post(
'/certificate/verify/front-end-libraries',
ifNoUser401,
verifyCert.bind(null, certTypes.frontEndLibs)
);
router.post(
'/certificate/verify/javascript-algorithms-data-structures',
ifNoUser401,
verifyCert.bind(null, certTypes.jsAlgoDataStruct)
);
router.post(
'/certificate/verify/data-visualization',
ifNoUser401,
verifyCert.bind(null, certTypes.dataVis)
);
router.post(
'/certificate/verify/apis-microservices',
ifNoUser401,
verifyCert.bind(null, certTypes.apisMicroservices)
);
router.post(
'/certificate/verify/information-security-quality-assurance',
ifNoUser401,
verifyCert.bind(null, certTypes.infosecQa)
);
router.post(
'/certificate/honest',
sendMessageToNonUser,
postHonest
ifNoSuperBlock404,
verifyCert
);
app.use(router);
function verifyCert(certType, req, res, next) {
const { user } = req;
const noNameMessage = dedent`
We need your name so we can put it on your certificate.
Add your name to your account settings and click the save button.
Then we can issue your certificate.
`;
const notCertifiedMessage = name => dedent`
it looks like you have not completed the neccessary 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 sucessfully claimed
the ${name}!
Congratulations on behalf of the freeCodeCamp team!
`;
function verifyCert(req, res, next) {
const { body: { superBlock }, user } = req;
let certType = superBlockCertTypeMap[superBlock];
log(certType);
if (certType === 'isDataVisCert') {
certType = 'is2018DataVisCert';
log(certType);
}
return user.getChallengeMap$()
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
const {
id,
tests,
name,
challengeType
} = challenge;
if (
user[certType] ||
!isCertified(tests, user.challengeMap)
) {
return Observable.just(false);
if (user[certType]) {
return Observable.just(alreadyClaimedMessage(name));
}
if (!user[certType] && !isCertified(tests, user.challengeMap)) {
return Observable.just(notCertifiedMessage(name));
}
if (!user.name) {
return Observable.just(noNameMessage);
}
const updateData = {
$set: {
@@ -232,49 +223,32 @@ export default function certificate(app) {
sendCertifiedEmail(user, Email.send$),
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
)
.map(
.map(
({ count, pledgeOrMessage }) => {
if (typeof pledgeOrMessage === 'string') {
log(pledgeOrMessage);
}
log(`${count} documents updated`);
return true;
return successMessage(user.username, name);
}
);
})
})
.subscribe(
(didCertify) => {
if (didCertify) {
// Check if they have a name set
if (user.name === '') {
return res.status(200).send(
dedent`
We need your name so we can put it on your certificate.
<a href="https://github.com/settings/profile">Add your
name to your GitHub account</a>, then go to your
<a href="https://www.freecodecamp.org/settings">settings
page</a> and click the "update my portfolio from GitHub"
button. Then we can issue your certificate.
`
);
}
return res.status(200).send(true);
}
return res.status(200).send(
dedent`
Looks like you have not completed the neccessary steps.
Please return to the challenge map.
`
);
(message) => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
});
},
next
);
}
function postHonest(req, res, next) {
return req.user.update$({ $set: { isHonest: true } }).subscribe(
() => res.status(200).send(true),
next
);
function ifNoSuperBlock404(req, res, next) {
const { superBlock } = req.body;
if (superBlock && superBlocks.includes(superBlock)) {
return next();
}
return res.status(404).end();
}
}

View File

@@ -10,7 +10,7 @@ import { alertTypes } from '../../common/utils/flash.js';
export default function settingsController(app) {
const api = app.loopback.Router();
const toggleUserFlag = flag => (req, res, next) => {
const toggleUserFlag = (flag, req, res, next) => {
const { user } = req;
const currentValue = user[ flag ];
return user
@@ -24,6 +24,15 @@ export default function settingsController(app) {
);
};
function refetchChallengeMap(req, res, next) {
const { user } = req;
return user.requestChallengeMap()
.subscribe(
challengeMap => res.json({ challengeMap }),
next
);
}
const updateMyEmailValidators = [
check('email')
.isEmail()
@@ -39,14 +48,6 @@ export default function settingsController(app) {
);
}
api.post(
'/update-my-email',
ifNoUser401,
updateMyEmailValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyEmail
);
function updateMyLang(req, res, next) {
const { user, body: { lang } = {} } = req;
const langName = supportedLanguages[lang];
@@ -87,6 +88,94 @@ export default function settingsController(app) {
);
}
const updateMyThemeValidators = [
check('theme')
.isIn(Object.keys(themes))
.withMessage('Theme is invalid.')
];
function updateMyTheme(req, res, next) {
const { body: { theme } } = req;
if (req.user.theme === theme) {
return res.sendFlash(alertTypes.info, 'Theme already set');
}
return req.user.updateTheme(theme)
.then(
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
next
);
}
function updateFlags(req, res, next) {
const { user, body: { values } } = req;
const keys = Object.keys(values);
if (
keys.length === 1 &&
typeof keys[0] === 'boolean'
) {
return toggleUserFlag(keys[0], req, res, next);
}
return user.requestUpdateFlags(values)
.subscribe(
message => res.json({ message }),
next
);
}
function updateMyPortfolio(req, res, next) {
const {
user,
body: { portfolio }
} = req;
// if we only have one key, it should be the id
// user cannot send only one key to this route
// other than to remove a portfolio item
const requestDelete = Object.keys(portfolio).length === 1;
return user.updateMyPortfolio(portfolio, requestDelete)
.subscribe(
message => res.json({ message }),
next
);
}
function updateMyProjects(req, res, next) {
const {
user,
body: { projects: project }
} = req;
return user.updateMyProjects(project)
.subscribe(
message => res.json({ message }),
next
);
}
function updateMyUsername(req, res, next) {
const { user, body: { username } } = req;
return user.updateMyUsername(username)
.subscribe(
message => res.json({ message }),
next
);
}
api.post(
'/refetch-user-challenge-map',
ifNoUser401,
refetchChallengeMap
);
api.post(
'/update-flags',
ifNoUser401,
updateFlags
);
api.post(
'/update-my-email',
ifNoUser401,
updateMyEmailValidators,
createValidatorErrorHandler(alertTypes.danger),
updateMyEmail
);
api.post(
'/update-my-current-challenge',
ifNoUser401,
@@ -94,23 +183,21 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyCurrentChallenge
);
const updateMyThemeValidators = [
check('theme')
.isIn(Object.keys(themes))
.withMessage('Theme is invalid.')
];
function updateMyTheme(req, res, next) {
const { body: { theme } } = req;
if (req.user.theme === theme) {
return res.sendFlash(alertTypes.info, 'Theme already set');
}
return req.user.updateTheme(theme)
.then(
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
next
);
}
api.post(
'/update-my-lang',
ifNoUser401,
updateMyLang
);
api.post(
'/update-my-portfolio',
ifNoUser401,
updateMyPortfolio
);
api.post(
'/update-my-projects',
ifNoUser401,
updateMyProjects
);
api.post(
'/update-my-theme',
ifNoUser401,
@@ -118,36 +205,10 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyTheme
);
api.post(
'/toggle-available-for-hire',
'/update-my-username',
ifNoUser401,
toggleUserFlag('isAvailableForHire')
);
api.post(
'/toggle-lockdown',
ifNoUser401,
toggleUserFlag('isLocked')
);
api.post(
'/toggle-announcement-email',
ifNoUser401,
toggleUserFlag('sendMonthlyEmail')
);
api.post(
'/toggle-notification-email',
ifNoUser401,
toggleUserFlag('sendNotificationEmail')
);
api.post(
'/toggle-quincy-email',
ifNoUser401,
toggleUserFlag('sendQuincyEmail')
);
api.post(
'/update-my-lang',
ifNoUser401,
updateMyLang
updateMyUsername
);
app.use(api);

View File

@@ -2,6 +2,7 @@ import dedent from 'dedent';
import moment from 'moment-timezone';
import { Observable } from 'rx';
import debugFactory from 'debug';
// import { curry } from 'lodash';
import emoji from 'node-emoji';
import {
@@ -11,10 +12,12 @@ import {
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
ifNoUser401,
ifNoUserRedirectTo,
@@ -32,6 +35,7 @@ import { getChallengeInfo, cachedMap } from '../utils/map';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
// const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.backEnd]: backEndChallengeId,
@@ -39,6 +43,7 @@ const certIds = {
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
@@ -52,6 +57,7 @@ const certViews = {
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
@@ -66,6 +72,7 @@ const certText = {
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
@@ -160,11 +167,6 @@ module.exports = function(app) {
);
}
router.get(
'/delete-my-account',
sendNonUserToMap,
showDelete
);
api.post(
'/account/delete',
ifNoUser401,
@@ -175,17 +177,11 @@ module.exports = function(app) {
sendNonUserToMap,
getAccount
);
router.get(
'/reset-my-progress',
sendNonUserToMap,
showResetProgress
);
api.post(
'/account/resetprogress',
'/account/reset-progress',
ifNoUser401,
postResetProgress
);
api.get(
'/account/unlink/:social',
sendNonUserToMap,
@@ -194,48 +190,8 @@ module.exports = function(app) {
// Ensure these are the last routes!
api.get(
'/:username/front-end-certification',
showCert.bind(null, certTypes.frontEnd)
);
api.get(
'/:username/back-end-certification',
showCert.bind(null, certTypes.backEnd)
);
api.get(
'/:username/full-stack-certification',
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
);
api.get(
'/:username/responsive-web-design-certification',
showCert.bind(null, certTypes.respWebDesign)
);
api.get(
'/:username/front-end-libraries-certification',
showCert.bind(null, certTypes.frontEndLibs)
);
api.get(
'/:username/javascript-algorithms-data-structures-certification',
showCert.bind(null, certTypes.jsAlgoDataStruct)
);
api.get(
'/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis)
);
api.get(
'/:username/apis-microservices-certification',
showCert.bind(null, certTypes.apisMicroservices)
);
api.get(
'/:username/information-security-quality-assurance-certification',
showCert.bind(null, certTypes.infosecQa)
'/c/:username/:cert',
showCert
);
router.get('/:username', showUserProfile);
@@ -410,14 +366,14 @@ module.exports = function(app) {
);
}
function showCert(certType, req, res, next) {
const username = req.params.username.toLowerCase();
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(username, {
isGithubCool: true,
isCheater: true,
isLocked: true,
isAvailableForHire: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
@@ -425,6 +381,7 @@ module.exports = function(app) {
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
@@ -434,6 +391,7 @@ module.exports = function(app) {
})
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
@@ -441,15 +399,16 @@ module.exports = function(app) {
);
return res.redirect('/');
}
if (!user.isGithubCool) {
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to link GitHub with their account
This user needs to add their name to their account
in order for others to be able to view their certificate.
`
);
return res.redirect('back');
return res.redirect(profile);
}
if (user.isCheater) {
@@ -465,20 +424,20 @@ module.exports = function(app) {
in order for others to be able to view their certificate.
`
);
return res.redirect('back');
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect('back');
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
@@ -495,51 +454,49 @@ module.exports = function(app) {
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect('back');
return res.redirect(profile);
},
next
);
}
function showDelete(req, res) {
return res.render('account/delete', { title: 'Delete My Account!' });
}
function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) {
if (err) { return next(err); }
req.logout();
req.flash('info', 'You\'ve successfully deleted your account.');
return res.redirect('/');
});
}
function showResetProgress(req, res) {
return res.render('account/reset-progress', { title: 'Reset My Progress!'
req.flash('success', 'You have successfully deleted your account.');
return res.status(200).end();
});
}
function postResetProgress(req, res, next) {
User.findById(req.user.id, function(err, user) {
if (err) { return next(err); }
return user.updateAttributes({
return user.update$({
progressTimestamps: [{
timestamp: Date.now()
}],
currentStreak: 0,
longestStreak: 0,
currentChallengeId: '',
isBackEndCert: false,
isFullStackCert: false,
isDataVisCert: false,
isRespWebDesignCert: false,
is2018DataVisCert: false,
isFrontEndLibsCert: false,
isJsAlgoDataStructCert: false,
isApisMicroservicesCert: false,
isInfosecQaCert: false,
is2018FullStackCert: false,
isFrontEndCert: false,
challengeMap: {},
challegesCompleted: []
}, function(err) {
if (err) { return next(err); }
req.flash('info', 'You\'ve successfully reset your progress.');
return res.redirect('/');
});
isBackEndCert: false,
isDataVisCert: false,
isFullStackCert: false,
challengeMap: {}
})
.subscribe(
() => {
req.flash('success', 'You have successfully reset your progress.');
return res.status(200).end();
},
next
);
});
}

View File

@@ -1,44 +1,10 @@
import _ from 'lodash';
// import debug from 'debug';
// use old rxjs
import { Observable } from 'rx';
import _ from 'lodash';
const publicUserProps = [
'id',
'name',
'username',
'bio',
'theme',
'picture',
'points',
'email',
'languageTag',
'isCheater',
'isGithubCool',
'isLocked',
'isAvailableForHire',
'isFrontEndCert',
'isBackEndCert',
'isDataVisCert',
'isFullStackCert',
'isRespWebDesignCert',
'isFrontEndLibsCert',
'isJsAlgoDataStructCert',
'isApisMicroservicesCert',
'isInfosecQaCert',
'githubURL',
'sendMonthlyEmail',
'sendNotificationEmail',
'sendQuincyEmail',
'currentChallengeId',
'challengeMap'
];
// const log = debug('fcc:services:user');
import {
userPropsForSession,
normaliseUserFields
} from '../utils/publicUserProps';
export default function userServices() {
return {
@@ -51,18 +17,23 @@ export default function userServices() {
Observable.defer(() => user.getChallengeMap$())
.map(challengeMap => ({ ...user.toJSON(), challengeMap }))
.map(user => ({
entities: {
user: {
[user.username]: {
..._.pick(user, publicUserProps),
isTwitter: !!user.twitter,
isLinkedIn: !!user.linkedIn
entities: {
user: {
[user.username]: {
..._.pick(user, userPropsForSession),
isEmailVerified: !!user.emailVerified,
isGithub: !!user.githubURL,
isLinkedIn: !!user.linkedIn,
isTwitter: !!user.twitter,
isWebsite: !!user.website,
...normaliseUserFields(user)
}
}
}
},
result: user.username
}))
)
},
result: user.username
})
)
)
.subscribe(
user => cb(null, user),
cb

View File

@@ -1,11 +1,12 @@
{
"frontEnd": "isFrontEndCert",
"backEnd": "isBackEndCert",
"dataVis": "isDataVisCert",
"fullStack": "isFullStackCert",
"respWebDesign": "isRespWebDesignCert",
"frontEndLibs": "isFrontEndLibsCert",
"dataVis2018": "is2018DataVisCert",
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
"dataVis": "isDataVisCert",
"apisMicroservices": "isApisMicroservicesCert",
"infosecQa": "isInfosecQaCert"
}
}

View File

@@ -1,11 +1,14 @@
{
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
"frontEndChallengeId": "561add10cb82ac38a17513be",
"backEndChallengeId": "660add10cb82ac38a17513be",
"dataVisId": "561add10cb82ac39a17513bc",
"respWebDesignId": "561add10cb82ac38a17513bc",
"frontEndLibsId": "561acd10cb82ac38a17513bc",
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
"dataVisId": "561add10cb82ac39a17513bc",
"apisMicroservicesId": "561add10cb82ac38a17523bc",
"infosecQaId": "561add10cb82ac38a17213bc"
}

View File

@@ -20,3 +20,7 @@ export function unDasherize(name) {
.replace(/[^a-zA-Z\d\s]/g, '')
.trim();
}
export function addPlaceholderImage(name) {
return `https://identicon.org?t=${name}&s=256`;
}

View File

@@ -0,0 +1,85 @@
import { isURL } from 'validator';
import { addPlaceholderImage } from '../utils';
import {
prepUniqueDaysByHours,
calcCurrentStreak,
calcLongestStreak
} from '../utils/user-stats';
export const publicUserProps = [
'about',
'calendar',
'challengeMap',
'githubURL',
'isApisMicroservicesCert',
'isBackEndCert',
'isCheater',
'isDataVisCert',
'isFrontEndCert',
'isFullStackCert',
'isFrontEndLibsCert',
'isGithubCool',
'isHonest',
'isInfosecQaCert',
'isJsAlgoDataStructCert',
'isLocked',
'isRespWebDesignCert',
'linkedin',
'location',
'name',
'points',
'portfolio',
'projects',
'streak',
'twitter',
'username',
'website'
];
export const userPropsForSession = [
...publicUserProps,
'currentChallengeId',
'email',
'id',
'languageTag',
'sendQuincyEmail',
'theme'
];
export function normaliseUserFields(user) {
const about = user.bio && !user.about ? user.bio : user.about;
const picture = user.picture || addPlaceholderImage(user.username);
const twitter = user.twitter && isURL(user.twitter) ?
user.twitter :
user.twitter && `https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
return { about, picture, twitter };
}
export function getProgress(progressTimestamps, timezone = 'EST') {
const calendar = progressTimestamps
.map((objOrNum) => {
return typeof objOrNum === 'number' ?
objOrNum :
objOrNum.timestamp;
})
.filter((timestamp) => {
return !!timestamp;
})
.reduce((data, timeStamp) => {
data[Math.floor(timeStamp / 1000)] = 1;
return data;
}, {});
const timestamps = progressTimestamps
.map(objOrNum => {
return typeof objOrNum === 'number' ?
objOrNum :
objOrNum.timestamp;
});
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
const streak = {
longest: calcLongestStreak(uniqueHours, timezone),
current: calcCurrentStreak(uniqueHours, timezone)
};
return { calendar, streak };
}

View File

@@ -0,0 +1,19 @@
import certTypes from './certTypes.json';
const superBlockCertTypeMap = {
// legacy
'front-end': certTypes.frontEnd,
'back-end': certTypes.backEnd,
'data-visualization': certTypes.dataVis,
'full-stack': certTypes.fullStack,
// modern
'responsive-web-design': certTypes.respWebDesign,
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
'front-end-libraries': certTypes.frontEndLibs,
'data-visualization-2018': certTypes.dataVis2018,
'apis-and-microservices': certTypes.apisMicroservices,
'information-security-and-quality-assurance': certTypes.infosecQa
};
export default superBlockCertTypeMap;

View File

@@ -1,8 +1,8 @@
Thank you for updating your contact details.
Please verify your email by following the link below:
Please verify your new email by following the link below:
<a href="<%= verifyHref %>"><%= verifyHref %></a>
<%= host %>/passwordless-change?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %>
Happy coding!