diff --git a/common/app/App.jsx b/common/app/App.jsx
index 11dde49f91..f4e17b61f6 100644
--- a/common/app/App.jsx
+++ b/common/app/App.jsx
@@ -16,6 +16,7 @@ import Toasts from './Toasts';
import NotFound from './NotFound';
import { mainRouteSelector } from './routes/redux';
import Challenges from './routes/Challenges';
+import Profile from './routes/Profile';
import Settings from './routes/Settings';
const mapDispatchToProps = {
@@ -44,6 +45,7 @@ const propTypes = {
const routes = {
challenges: Challenges,
+ profile: Profile,
settings: Settings
};
diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx
index 930c6feb05..222c511196 100644
--- a/common/app/Nav/Nav.jsx
+++ b/common/app/Nav/Nav.jsx
@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
import capitalize from 'lodash/capitalize';
import { createSelector } from 'reselect';
import FCCSearchBar from 'react-freecodecamp-search';
-
import {
MenuItem,
Nav,
@@ -15,6 +14,7 @@ import {
NavbarBrand
} from 'react-bootstrap';
+import NoPropsPassThrough from '../utils/No-Props-Passthrough.jsx';
import { Link } from '../Router';
import navLinks from './links.json';
import SignUp from './Sign-Up.jsx';
@@ -30,6 +30,7 @@ import {
} from './redux';
import { isSignedInSelector, signInLoadingSelector } from '../redux';
import { panesSelector } from '../Panes/redux';
+import { onRouteCurrentChallenge } from '../routes/Challenges/redux';
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
@@ -224,11 +225,16 @@ export class FCCNav extends React.Component {
))
}
{ shouldShowMapButton ?
-
+ { + 'In order to view their progress through the freeCodeCamp ' + + 'curriculum, they need to make all of thie solutions public' + } +
++ { `We could not find a user by the name of "${paramsUsername}"` } +
++ + { cert.title } + +
+ ++ No certificates have been earned under the current curriculum +
+ } + { + hasLegacyCert ? ++ { description } +
++ No challenges have been completed yet. + + Get started here. + +
: +Challenge | +First Completed | +Last Changed | ++ |
---|
{ name }
} { location &&{ location }
} diff --git a/common/app/routes/Settings/settings.less b/common/app/routes/Settings/settings.less index 8fc27c8ebd..47990ee87f 100644 --- a/common/app/routes/Settings/settings.less +++ b/common/app/routes/Settings/settings.less @@ -24,6 +24,11 @@ color: #333; } + a:hover { + text-decoration: none; + text-decoration-line: none; + } + .panel { background-color: #fff; } diff --git a/common/app/routes/index.js b/common/app/routes/index.js index c925d4ee64..e007db83d1 100644 --- a/common/app/routes/index.js +++ b/common/app/routes/index.js @@ -1,6 +1,7 @@ import { routes as challengesRoutes } from './Challenges'; import { routes as mapRoutes } from './Map'; import { routes as settingsRoutes } from './Settings'; +import { routes as profileRoutes } from './Profile'; // import { addLang } from '../utils/lang'; @@ -9,7 +10,9 @@ export { createPanesMap } from './Challenges'; export default { ...challengesRoutes, ...mapRoutes, - ...settingsRoutes + ...settingsRoutes, + // ensure profile routes are last else they hijack other routes + ...profileRoutes }; // export default function createChildRoute(deps) { diff --git a/common/app/routes/index.less b/common/app/routes/index.less index ae31c119f1..75b2783b84 100644 --- a/common/app/routes/index.less +++ b/common/app/routes/index.less @@ -1,2 +1,3 @@ &{ @import "./Challenges/challenges.less"; } +&{ @import "./Profile/profile.less"; } &{ @import "./Settings/index.less"; } diff --git a/common/app/routes/redux.js b/common/app/routes/redux.js index f0f82a875d..737987ea24 100644 --- a/common/app/routes/redux.js +++ b/common/app/routes/redux.js @@ -3,8 +3,10 @@ import { combineReducers } from 'berkeleys-redux-utils'; import challengeReducer from './Challenges/redux'; +import profileReducer from './Profile/redux'; import settingsReducer from './Settings/redux'; import { routes as challengeRoutes } from './Challenges'; +import { routes as profileRoutes } from './Profile'; import { routes as settingsRoutes } from './Settings'; const ns = 'mainRouter'; @@ -22,6 +24,9 @@ export function mainRouter(state = 'NotFound', action) { if (settingsRoutes[type]) { return 'settings'; } + if (profileRoutes[type]) { + return 'profile'; + } return ''; } @@ -29,6 +34,7 @@ mainRouter.toString = () => ns; export default combineReducers( challengeReducer, + profileReducer, settingsReducer, mainRouter ); diff --git a/common/models/user.js b/common/models/user.js index 135129c536..8b1401a4b1 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -17,6 +17,11 @@ import { getServerFullURL, getEmailSender } from '../../server/utils/url-utils.js'; +import { + normaliseUserFields, + getProgress, + publicUserProps +} from '../../server/utils/publicUserProps'; const debug = debugFactory('fcc:models:user'); const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; @@ -760,6 +765,57 @@ module.exports = function(User) { }); }; + User.getPublicProfile = function getPublicProfile(username, cb) { + return User.findOne$({ where: { username }}) + .flatMap(user => { + if (!user) { + return Observable.of({}); + } + const { challengeMap, progressTimestamps, timezone } = user; + return Observable.of({ + entities: { + user: { + [user.username]: { + ..._.pick(user, publicUserProps), + isGithub: !!user.githubURL, + isLinkedIn: !!user.linkedIn, + isTwitter: !!user.twitter, + isWebsite: !!user.website, + points: progressTimestamps.length, + challengeMap, + ...getProgress(progressTimestamps, timezone), + ...normaliseUserFields(user) + } + } + }, + result: user.username + }); + }) + .subscribe( + user => cb(null, user), + cb + ); + }; + + User.remoteMethod('getPublicProfile', { + accepts: { + arg: 'username', + type: 'string', + required: true + }, + returns: [ + { + arg: 'user', + type: 'object', + root: true + } + ], + http: { + path: '/get-public-profile', + verb: 'GET' + } + }); + User.giveBrowniePoints = function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { const findUser = observeMethod(User, 'findOne'); diff --git a/common/models/user.json b/common/models/user.json index 2a5ee0cd59..18949a58ec 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -348,6 +348,13 @@ "permission": "ALLOW", "property": "about" }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW", + "property": "getPublicProfile" + }, { "accessType": "EXECUTE", "principalType": "ROLE", diff --git a/package-lock.json b/package-lock.json index 51d39364a6..e221e0a6d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13990,6 +13990,15 @@ } } }, + "react-d3": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/react-d3/-/react-d3-0.4.0.tgz", + "integrity": "sha1-3s7c7ZZ/SM2JzNeftAjUfw8qy+I=", + "requires": { + "d3": "3.5.17", + "react": "15.6.2" + } + }, "react-dom": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", diff --git a/package.json b/package.json index 16dae80e16..7e2abaa488 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "react-addons-shallow-compare": "~15.4.2", "react-bootstrap": "~0.31.2", "react-codemirror": "^0.3.0", + "react-d3": "^0.4.0", "react-dom": "^15.6.2", "react-fontawesome": "^1.2.0", "react-freecodecamp-search": "^1.4.1", diff --git a/public/css/cal-heatmap.css b/public/css/cal-heatmap.css new file mode 100644 index 0000000000..ceb3519ecd --- /dev/null +++ b/public/css/cal-heatmap.css @@ -0,0 +1,145 @@ +/* Cal-HeatMap CSS */ + +.cal-heatmap-container { + display: block; +} + +.cal-heatmap-container .graph +{ + font-family: "Lucida Grande", Lucida, Verdana, sans-serif; +} + +.cal-heatmap-container .graph-label +{ + fill: #999; + font-size: 10px +} + +.cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect { + shape-rendering: crispedges +} + +.cal-heatmap-container .graph-rect +{ + fill: #ededed +} + +.cal-heatmap-container .graph-subdomain-group rect:hover +{ + stroke: #000; + stroke-width: 1px +} + +.cal-heatmap-container .subdomain-text { + font-size: 8px; + fill: #999; + pointer-events: none +} + +.cal-heatmap-container .hover_cursor:hover { + cursor: pointer +} + +.cal-heatmap-container .qi { + background-color: #999; + fill: #999 +} + +/* +Remove comment to apply this style to date with value equal to 0 +.q0 +{ + background-color: #fff; + fill: #fff; + stroke: #ededed +} +*/ + +.cal-heatmap-container .q1 +{ + background-color: #dae289; + fill: #dae289 +} + +.cal-heatmap-container .q2 +{ + background-color: #cedb9c; + fill: #9cc069 +} + +.cal-heatmap-container .q3 +{ + background-color: #b5cf6b; + fill: #669d45 +} + +.cal-heatmap-container .q4 +{ + background-color: #637939; + fill: #637939 +} + +.cal-heatmap-container .q5 +{ + background-color: #3b6427; + fill: #3b6427 +} + +.cal-heatmap-container rect.highlight +{ + stroke:#444; + stroke-width:1 +} + +.cal-heatmap-container text.highlight +{ + fill: #444 +} + +.cal-heatmap-container rect.now +{ + stroke: red +} + +.cal-heatmap-container text.now +{ + fill: red; + font-weight: 800 +} + +.cal-heatmap-container .domain-background { + fill: none; + shape-rendering: crispedges +} + +.ch-tooltip { + padding: 10px; + background: #222; + color: #bbb; + font-size: 12px; + line-height: 1.4; + width: 140px; + position: absolute; + z-index: 99999; + text-align: center; + border-radius: 2px; + box-shadow: 2px 2px 2px rgba(0,0,0,0.2); + display: none; + box-sizing: border-box; +} + +.ch-tooltip::after{ + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + content: ""; + padding: 0; + display: block; + bottom: -6px; + left: 50%; + margin-left: -6px; + border-width: 6px 6px 0; + border-top-color: #222; +} diff --git a/server/boot/react.js b/server/boot/react.js index 7cc58e614c..bb25e2db0a 100644 --- a/server/boot/react.js +++ b/server/boot/react.js @@ -21,7 +21,8 @@ const routes = [ '/challenges/*', '/map', '/settings', - '/settings/*' + '/settings/*', + '/:username' ]; const devRoutes = []; diff --git a/server/boot/user.js b/server/boot/user.js index ecddb568b8..add4f91cb5 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -1,9 +1,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 { curry } from 'lodash'; import { frontEndChallengeId, @@ -24,18 +22,10 @@ import { ifNotVerifiedRedirectToSettings } from '../utils/middleware'; import { observeQuery } from '../utils/rx'; -import { - prepUniqueDaysByHours, - calcCurrentStreak, - calcLongestStreak -} from '../utils/user-stats'; -import supportedLanguages from '../../common/utils/supported-languages'; -import { encodeFcc } from '../../common/utils/encode-decode.js'; -import { getChallengeInfo, cachedMap } from '../utils/map'; const debug = debugFactory('fcc:boot:user'); const sendNonUserToMap = ifNoUserRedirectTo('/map'); -// const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map'); +const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map'); const certIds = { [certTypes.frontEnd]: frontEndChallengeId, [certTypes.backEnd]: backEndChallengeId, @@ -77,84 +67,10 @@ const certText = { [certTypes.infosecQa]: 'Information Security and Quality Assurance Certified' }; -const dateFormat = 'MMM DD, YYYY'; - -function isAlgorithm(challenge) { - // test if name starts with hike/waypoint/basejump/zipline - // fix for bug that saved different challenges with incorrect - // challenge types - return !(/^(waypoint|hike|zipline|basejump)/i).test(challenge.name) && - +challenge.challengeType === 5; -} - -function isProject(challenge) { - return +challenge.challengeType === 3 || - +challenge.challengeType === 4; -} - -function getChallengeGroup(challenge) { - if (isProject(challenge)) { - return 'projects'; - } else if (isAlgorithm(challenge)) { - return 'algorithms'; - } - return 'challenges'; -} - -// buildDisplayChallenges( -// entities: { challenge: Object, challengeIdToName: Object }, -// challengeMap: Object, -// tz: String -// ) => Observable[{ -// algorithms: Array, -// projects: Array, -// challenges: Array -// }] -function buildDisplayChallenges( - { challengeMap, challengeIdToName }, - userChallengeMap = {}, - timezone -) { - return Observable.from(Object.keys(userChallengeMap)) - .map(challengeId => userChallengeMap[challengeId]) - .map(userChallenge => { - const challengeId = userChallenge.id; - const challenge = challengeMap[ challengeIdToName[challengeId] ]; - let finalChallenge = { ...userChallenge, ...challenge }; - if (userChallenge.completedDate) { - finalChallenge.completedDate = moment - .tz(userChallenge.completedDate, timezone) - .format(dateFormat); - } - - if (userChallenge.lastUpdated) { - finalChallenge.lastUpdated = moment - .tz(userChallenge.lastUpdated, timezone) - .format(dateFormat); - } - - return finalChallenge; - }) - .filter(({ challengeType }) => challengeType !== 6) - .groupBy(getChallengeGroup) - .flatMap(group$ => { - return group$.toArray().map(challenges => ({ - [getChallengeGroup(challenges[0])]: challenges - })); - }) - .reduce((output, group) => ({ ...output, ...group}), {}) - .map(groups => ({ - algorithms: groups.algorithms || [], - projects: groups.projects ? groups.projects.reverse() : [], - challenges: groups.challenges ? groups.challenges.reverse() : [] - })); -} - module.exports = function(app) { const router = app.loopback.Router(); const api = app.loopback.Router(); const { Email, User } = app.models; - const map$ = cachedMap(app.models); function findUserByUsername$(username, fields) { return observeQuery( @@ -194,16 +110,15 @@ module.exports = function(app) { showCert ); - router.get('/:username', showUserProfile); router.get( - '/:username/report-user/', - sendNonUserToMap, + '/user/:username/report-user/', + sendNonUserToMapWithMessage('You must be signed in to report a user'), ifNotVerifiedRedirectToSettings, getReportUserProfile ); api.post( - '/:username/report-user/', + '/user/:username/report-user/', ifNoUser401, postReportUserProfile ); @@ -270,102 +185,6 @@ module.exports = function(app) { }); } - function showUserProfile(req, res, next) { - const username = req.params.username.toLowerCase(); - const { user } = req; - - // timezone of signed-in account - // to show all date related components - // using signed-in account's timezone - // not of the profile she is viewing - const timezone = user && user.timezone ? - user.timezone : - 'EST'; - - const query = { - where: { username }, - include: 'pledge' - }; - - return User.findOne$(query) - .filter(userPortfolio => { - if (!userPortfolio) { - next(); - } - return !!userPortfolio; - }) - .flatMap(userPortfolio => { - userPortfolio = userPortfolio.toJSON(); - - const timestamps = userPortfolio - .progressTimestamps - .map(objOrNum => { - return typeof objOrNum === 'number' ? - objOrNum : - objOrNum.timestamp; - }); - - const uniqueHours = prepUniqueDaysByHours(timestamps, timezone); - - userPortfolio.currentStreak = calcCurrentStreak(uniqueHours, timezone); - userPortfolio.longestStreak = calcLongestStreak(uniqueHours, timezone); - - const calender = userPortfolio - .progressTimestamps - .map((objOrNum) => { - return typeof objOrNum === 'number' ? - objOrNum : - objOrNum.timestamp; - }) - .filter((timestamp) => { - return !!timestamp; - }) - .reduce((data, timeStamp) => { - data[(timeStamp / 1000)] = 1; - return data; - }, {}); - - if (userPortfolio.isCheater && !user) { - req.flash( - 'danger', - dedent` - Upon review, this account has been flagged for academic - dishonesty. If you’re the owner of this account contact - team@freecodecamp.org for details. - ` - ); - } - - if (userPortfolio.bio) { - userPortfolio.bio = emoji.emojify(userPortfolio.bio); - } - - return getChallengeInfo(map$) - .flatMap(challengeInfo => buildDisplayChallenges( - challengeInfo, - userPortfolio.challengeMap, - timezone - )) - .map(displayChallenges => ({ - ...userPortfolio, - ...displayChallenges, - title: 'Camper ' + userPortfolio.username + '\'s Code Portfolio', - calender, - github: userPortfolio.githubURL, - moment, - encodeFcc, - supportedLanguages - })); - }) - .doOnNext(data => { - return res.render('account/show', data); - }) - .subscribe( - () => {}, - next - ); - } - function showCert(req, res, next) { let { username, cert } = req.params; username = username.toLowerCase(); diff --git a/server/services/user.js b/server/services/user.js index e191f74984..cd1fc61098 100644 --- a/server/services/user.js +++ b/server/services/user.js @@ -2,21 +2,40 @@ import { Observable } from 'rx'; import _ from 'lodash'; import { - userPropsForSession, - normaliseUserFields + getProgress, + normaliseUserFields, + userPropsForSession } from '../utils/publicUserProps'; export default function userServices() { return { name: 'user', - read: (req, resource, params, config, cb) => { - const { user } = req; + read: function readUserService( + req, + resource, + params, + config, + cb) { + const queryUser = req.user; + const source = queryUser && Observable.forkJoin( + queryUser.getChallengeMap$(), + queryUser.getPoints$(), + (challengeMap, progressTimestamps) => ({ + challengeMap, + progress: getProgress(progressTimestamps, queryUser.timezone) + }) + ); Observable.if( - () => !user, + () => !queryUser, Observable.of({}), - Observable.defer(() => user.getChallengeMap$()) - .map(challengeMap => ({ ...user.toJSON(), challengeMap })) - .map(user => ({ + Observable.defer(() => source) + .map(({ challengeMap, progress }) => ({ + ...queryUser.toJSON(), + ...progress, + challengeMap + })) + .map( + user => ({ entities: { user: { [user.username]: { diff --git a/server/utils/publicUserProps.js b/server/utils/publicUserProps.js index def0a6bbf8..964cad0ab9 100644 --- a/server/utils/publicUserProps.js +++ b/server/utils/publicUserProps.js @@ -1,6 +1,6 @@ import { isURL } from 'validator'; -import { addPlaceholderImage } from '../utils'; +import { addPlaceholderImage } from './'; import { prepUniqueDaysByHours, calcCurrentStreak, diff --git a/server/views/certificate/advanced-front-end.jade b/server/views/certificate/advanced-front-end.jade index 1d2e18bb86..78f781f4b4 100644 --- a/server/views/certificate/advanced-front-end.jade +++ b/server/views/certificate/advanced-front-end.jade @@ -21,7 +21,7 @@ include styles h1 strong Advanced Frontend Projects h4 1 of 3 legacy freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/advanced-front-end-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/advanced-front-end-certification diff --git a/server/views/certificate/apis-and-microservices.jade b/server/views/certificate/apis-and-microservices.jade index 56e8d2fc3d..6a37a5722a 100644 --- a/server/views/certificate/apis-and-microservices.jade +++ b/server/views/certificate/apis-and-microservices.jade @@ -21,7 +21,7 @@ include styles h1 strong APIs and Microservices Projects h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/apis-and-microservices-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/apis-and-microservices-certification diff --git a/server/views/certificate/back-end.jade b/server/views/certificate/back-end.jade index 1d2bf7631f..e94d39a4ad 100644 --- a/server/views/certificate/back-end.jade +++ b/server/views/certificate/back-end.jade @@ -21,7 +21,7 @@ include styles h1 strong Back End Development Projects h4 1 of 3 legacy freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/back-end-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/back-end-certification diff --git a/server/views/certificate/data-visualization.jade b/server/views/certificate/data-visualization.jade index 1831699fe0..0782d408aa 100644 --- a/server/views/certificate/data-visualization.jade +++ b/server/views/certificate/data-visualization.jade @@ -21,7 +21,7 @@ include styles h1 strong Data Visualization Projects h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/data-visualization-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/data-visualization-certification diff --git a/server/views/certificate/front-end-libraries.jade b/server/views/certificate/front-end-libraries.jade index 24d7dfad85..f5d4206fd2 100644 --- a/server/views/certificate/front-end-libraries.jade +++ b/server/views/certificate/front-end-libraries.jade @@ -21,7 +21,7 @@ include styles h1 strong Front End Libraries Projects h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/front-end-libraries-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-libraries-certification diff --git a/server/views/certificate/front-end.jade b/server/views/certificate/front-end.jade index efa363ce88..1179721bb0 100644 --- a/server/views/certificate/front-end.jade +++ b/server/views/certificate/front-end.jade @@ -21,7 +21,7 @@ include styles h1 strong Front End Development Projects h4 1 of 3 legacy freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/front-end-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-certification diff --git a/server/views/certificate/full-stack.jade b/server/views/certificate/full-stack.jade index 69fea248f7..30782f0137 100644 --- a/server/views/certificate/full-stack.jade +++ b/server/views/certificate/full-stack.jade @@ -21,7 +21,7 @@ include styles h1 strong Full Stack Development Projects h4 1 of 3 legacy freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/full-stack-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/full-stack-certification diff --git a/server/views/certificate/information-security-and-quality-assurance.jade b/server/views/certificate/information-security-and-quality-assurance.jade index 5d82fb7d24..0a71cf3b97 100644 --- a/server/views/certificate/information-security-and-quality-assurance.jade +++ b/server/views/certificate/information-security-and-quality-assurance.jade @@ -21,7 +21,7 @@ include styles h1 strong Information Security and Quality Assurance Projects h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/information-security-and-quality-assurance-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/information-security-and-quality-assurance-certification diff --git a/server/views/certificate/javascript-algorithms-and-data-structures.jade b/server/views/certificate/javascript-algorithms-and-data-structures.jade index 03b42c29aa..ebe72a1f44 100644 --- a/server/views/certificate/javascript-algorithms-and-data-structures.jade +++ b/server/views/certificate/javascript-algorithms-and-data-structures.jade @@ -21,7 +21,7 @@ include styles h1 strong JavaScript Algorithms and Data Structures Certificate h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/javascript-algorithms-and-data-structures-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/javascript-algorithms-and-data-structures-certification diff --git a/server/views/certificate/responsive-web-design.jade b/server/views/certificate/responsive-web-design.jade index b5ccbe321b..2873106ba0 100644 --- a/server/views/certificate/responsive-web-design.jade +++ b/server/views/certificate/responsive-web-design.jade @@ -21,7 +21,7 @@ include styles h1 strong Responsive Web Design Projects h4 1 of 6 freeCodeCamp certificates, 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") @@ -29,4 +29,4 @@ include styles strong Quincy Larson p Executive Director, freeCodeCamp.org .row - p.verify Verify this certificate at: https://freecodecamp.org/#{username}/responsive-web-design-certification + p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/responsive-web-design-certification