Merge pull request #16788 from Bouncey/feat/legacyCerts
Feat(legacy-certs): Claim legacy certificates from the settings page
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { findIndex, invert, pick, property, merge, union } from 'lodash';
|
||||
import { findIndex, property, merge, union } from 'lodash';
|
||||
import uuid from 'uuid/v4';
|
||||
import {
|
||||
combineActions,
|
||||
@ -12,6 +12,7 @@ import { themes } from '../../utils/themes';
|
||||
import { usernameSelector, types as app } from '../redux';
|
||||
import { types as challenges } from '../routes/Challenges/redux';
|
||||
import { types as map } from '../Map/redux';
|
||||
import legacyProjects from '../../utils/legacyProjectData';
|
||||
|
||||
export const ns = 'entities';
|
||||
export const getNS = state => state[ns];
|
||||
@ -91,10 +92,6 @@ const defaultState = {
|
||||
fullBlocks: []
|
||||
};
|
||||
|
||||
export function selectiveChallengeTitleSelector(state, dashedName) {
|
||||
return getNS(state).challenge[dashedName].title;
|
||||
}
|
||||
|
||||
export function portfolioSelector(state, props) {
|
||||
const username = usernameSelector(state);
|
||||
const { portfolio } = getNS(state).user[username];
|
||||
@ -103,27 +100,42 @@ export function portfolioSelector(state, props) {
|
||||
}
|
||||
|
||||
export function projectsSelector(state) {
|
||||
const blocks = getNS(state).block;
|
||||
const challengeNameToIdMap = invert(challengeIdToNameMapSelector(state));
|
||||
const {
|
||||
block: blocks,
|
||||
challenge: challengeMap
|
||||
} = getNS(state);
|
||||
const idToNameMap = challengeIdToNameMapSelector(state);
|
||||
const legacyWithDashedNames = legacyProjects
|
||||
.reduce((list, current) => ([
|
||||
...list,
|
||||
{
|
||||
...current,
|
||||
challenges: current.challenges.map(id => idToNameMap[id])
|
||||
}
|
||||
]),
|
||||
[]
|
||||
);
|
||||
return Object.keys(blocks)
|
||||
.filter(key =>
|
||||
key.includes('projects') && !key.includes('coding-interview')
|
||||
)
|
||||
.map(key => blocks[key])
|
||||
.concat(legacyWithDashedNames)
|
||||
.map(({ title, challenges, superBlock }) => {
|
||||
const projectChallengeDashNames = challenges
|
||||
// challengeIdToName is not available on appMount
|
||||
.filter(Boolean)
|
||||
// remove any project intros
|
||||
.filter(chal => !chal.includes('get-set-for'));
|
||||
const projectChallenges = projectChallengeDashNames
|
||||
.map(dashedName => selectiveChallengeTitleSelector(state, dashedName));
|
||||
.map(dashedName => {
|
||||
const { id, title } = challengeMap[dashedName];
|
||||
return { id, title, dashedName };
|
||||
});
|
||||
return {
|
||||
projectBlockName: title,
|
||||
superBlock,
|
||||
challenges: projectChallenges,
|
||||
challengeNameIdMap: pick(
|
||||
challengeNameToIdMap,
|
||||
projectChallengeDashNames
|
||||
)
|
||||
challenges: projectChallenges
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ const mapStateToProps = createSelector(
|
||||
{
|
||||
show: is2018DataVisCert,
|
||||
title: 'Data Visualization Certificate:',
|
||||
showURL: 'data-visualization-2018'
|
||||
showURL: 'data-visualization'
|
||||
},
|
||||
{
|
||||
show: isApisMicroservicesCert,
|
||||
@ -78,22 +78,22 @@ const mapStateToProps = createSelector(
|
||||
{
|
||||
show: isFullStackCert,
|
||||
title: 'Full Stack Certificate:',
|
||||
showURL: 'full-stack'
|
||||
showURL: 'legacy-full-stack'
|
||||
},
|
||||
{
|
||||
show: isFrontEndCert,
|
||||
title: 'Front End Certificate:',
|
||||
showURL: 'front-end'
|
||||
showURL: 'legacy-front-end'
|
||||
},
|
||||
{
|
||||
show: isBackEndCert,
|
||||
title: 'Back End Certificate:',
|
||||
showURL: 'back-end'
|
||||
showURL: 'legacy-back-end'
|
||||
},
|
||||
{
|
||||
show: isDataVisCert,
|
||||
title: 'Data Visualization Certificate:',
|
||||
showURL: 'data-visualization'
|
||||
showURL: 'legacy-data-visualization'
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -134,7 +134,7 @@ function renderCertShow(username, cert) {
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
href={ `/c/${username}/${cert.showURL}`}
|
||||
href={ `/certificates/${username}/${cert.showURL}`}
|
||||
>
|
||||
Show
|
||||
</Button>
|
||||
|
@ -12,7 +12,12 @@ import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx';
|
||||
import SectionHeader from './SectionHeader.jsx';
|
||||
import { projectsSelector } from '../../../entities';
|
||||
import { claimCert, updateUserBackend } from '../redux';
|
||||
import { fetchChallenges, userSelector, hardGoTo } from '../../../redux';
|
||||
import {
|
||||
fetchChallenges,
|
||||
userSelector,
|
||||
hardGoTo,
|
||||
createErrorObservable
|
||||
} from '../../../redux';
|
||||
import {
|
||||
buildUserProjectsMap,
|
||||
jsProjectSuperBlock
|
||||
@ -30,11 +35,16 @@ const mapStateToProps = createSelector(
|
||||
isJsAlgoDataStructCert,
|
||||
isApisMicroservicesCert,
|
||||
isInfosecQaCert,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isDataVisCert,
|
||||
username
|
||||
},
|
||||
projects
|
||||
) => ({
|
||||
projects,
|
||||
allProjects: projects,
|
||||
legacyProjects: projects.filter(p => p.superBlock.includes('legacy')),
|
||||
modernProjects: projects.filter(p => !p.superBlock.includes('legacy')),
|
||||
userProjects: projects
|
||||
.map(block => buildUserProjectsMap(block, challengeMap))
|
||||
.reduce((projects, current) => ({
|
||||
@ -49,7 +59,10 @@ const mapStateToProps = createSelector(
|
||||
'Front End Libraries Projects': isFrontEndLibsCert,
|
||||
'Data Visualization Projects': is2018DataVisCert,
|
||||
'API and Microservice Projects': isApisMicroservicesCert,
|
||||
'Information Security and Quality Assurance Projects': isInfosecQaCert
|
||||
'Information Security and Quality Assurance Projects': isInfosecQaCert,
|
||||
'Legacy Front End Projects': isFrontEndCert,
|
||||
'Legacy Back End Projects': isBackEndCert,
|
||||
'Legacy Data Visualization Projects': isDataVisCert
|
||||
},
|
||||
username
|
||||
})
|
||||
@ -58,23 +71,35 @@ const mapStateToProps = createSelector(
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({
|
||||
claimCert,
|
||||
createError: createErrorObservable,
|
||||
fetchChallenges,
|
||||
hardGoTo,
|
||||
updateUserBackend
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
const projectsTypes = PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
projectBlockName: PropTypes.string,
|
||||
challenges: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
dashedName: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
})
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
const propTypes = {
|
||||
allProjects: projectsTypes,
|
||||
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
|
||||
claimCert: PropTypes.func.isRequired,
|
||||
createError: PropTypes.func.isRequired,
|
||||
fetchChallenges: PropTypes.func.isRequired,
|
||||
hardGoTo: PropTypes.func.isRequired,
|
||||
projects: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
projectBlockName: PropTypes.string,
|
||||
challenges: PropTypes.arrayOf(PropTypes.string)
|
||||
})
|
||||
),
|
||||
legacyProjects: projectsTypes,
|
||||
modernProjects: projectsTypes,
|
||||
superBlock: PropTypes.string,
|
||||
updateUserBackend: PropTypes.func.isRequired,
|
||||
userProjects: PropTypes.objectOf(
|
||||
@ -92,50 +117,157 @@ class CertificationSettings extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.buildProjectForms = this.buildProjectForms.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { projects } = this.props;
|
||||
if (!projects.length) {
|
||||
const { modernProjects } = this.props;
|
||||
if (!modernProjects.length) {
|
||||
this.props.fetchChallenges();
|
||||
}
|
||||
}
|
||||
|
||||
buildProjectForms({
|
||||
projectBlockName,
|
||||
challenges,
|
||||
superBlock
|
||||
}) {
|
||||
const {
|
||||
blockNameIsCertMap,
|
||||
claimCert,
|
||||
hardGoTo,
|
||||
userProjects,
|
||||
username
|
||||
} = this.props;
|
||||
const isCertClaimed = blockNameIsCertMap[projectBlockName];
|
||||
const challengeTitles = challenges
|
||||
.map(challenge => challenge.title || 'Unknown Challenge');
|
||||
if (superBlock === jsProjectSuperBlock) {
|
||||
return (
|
||||
<JSAlgoAndDSForm
|
||||
challenges={ challengeTitles }
|
||||
claimCert={ claimCert }
|
||||
hardGoTo={ hardGoTo }
|
||||
isCertClaimed={ isCertClaimed }
|
||||
jsProjects={ userProjects[superBlock] }
|
||||
key={ superBlock }
|
||||
projectBlockName={ projectBlockName }
|
||||
superBlock={ superBlock }
|
||||
username={ username }
|
||||
/>
|
||||
);
|
||||
}
|
||||
const options = challengeTitles
|
||||
.reduce((options, current) => {
|
||||
options.types[current] = 'url';
|
||||
return options;
|
||||
}, { types: {} });
|
||||
|
||||
options.types.id = 'hidden';
|
||||
options.placeholder = false;
|
||||
|
||||
const userValues = userProjects[superBlock] || {};
|
||||
|
||||
if (!userValues.id) {
|
||||
userValues.id = superBlock;
|
||||
}
|
||||
|
||||
const initialValues = challengeTitles
|
||||
.reduce((accu, current) => ({
|
||||
...accu,
|
||||
[current]: ''
|
||||
}), {});
|
||||
|
||||
const completedProjects = _.values(userValues)
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
// minus 1 to account for the id
|
||||
.length - 1;
|
||||
|
||||
const fullForm = completedProjects === challengeTitles.length;
|
||||
return (
|
||||
<FullWidthRow key={superBlock}>
|
||||
<h3 className='project-heading'>{ projectBlockName }</h3>
|
||||
<Form
|
||||
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
|
||||
enableSubmit={ fullForm }
|
||||
formFields={ challengeTitles.concat([ 'id' ]) }
|
||||
hideButton={isCertClaimed}
|
||||
id={ superBlock }
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
...userValues
|
||||
}}
|
||||
options={ options }
|
||||
submit={ this.handleSubmit }
|
||||
/>
|
||||
{
|
||||
isCertClaimed ?
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
href={ `/certificates/${username}/${superBlock}`}
|
||||
target='_blank'
|
||||
>
|
||||
Show Certificate
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
<hr />
|
||||
</FullWidthRow>
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmit(values) {
|
||||
const { id } = values;
|
||||
const fullForm = _.values(values)
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
// 5 projects + 1 id prop
|
||||
.length === 6;
|
||||
const { allProjects } = this.props;
|
||||
let project = _.find(allProjects, { superBlock: id });
|
||||
if (!project) {
|
||||
// the submitted projects do not belong to current/legacy certificates
|
||||
return this.props.createError(
|
||||
new Error(
|
||||
'Submitted projects do not belong to either current or ' +
|
||||
'legacy certificates'
|
||||
)
|
||||
);
|
||||
}
|
||||
const valuesSaved = _.values(this.props.userProjects[id])
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
.length === 6;
|
||||
if (fullForm && valuesSaved) {
|
||||
.filter(_.isString);
|
||||
|
||||
// minus 1 due to the form id being in values
|
||||
const isProjectSectionComplete =
|
||||
(valuesSaved.length - 1) === project.challenges.length;
|
||||
|
||||
if (isProjectSectionComplete) {
|
||||
return this.props.claimCert(id);
|
||||
}
|
||||
const { projects } = this.props;
|
||||
const pIndex = _.findIndex(projects, p => p.superBlock === id);
|
||||
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
|
||||
const valuesToIds = project.challenges
|
||||
.reduce((valuesMap, current) => {
|
||||
const solution = values[current.title];
|
||||
if (solution) {
|
||||
return {
|
||||
...valuesMap,
|
||||
[current.id]: solution
|
||||
};
|
||||
}
|
||||
return valuesMap;
|
||||
}, {});
|
||||
return this.props.updateUserBackend({
|
||||
projects: {
|
||||
[id]: values
|
||||
[id]: valuesToIds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
blockNameIsCertMap,
|
||||
claimCert,
|
||||
hardGoTo,
|
||||
projects,
|
||||
userProjects,
|
||||
username
|
||||
modernProjects,
|
||||
legacyProjects
|
||||
} = this.props;
|
||||
if (!projects.length) {
|
||||
if (!modernProjects.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@ -150,88 +282,14 @@ class CertificationSettings extends PureComponent {
|
||||
you can claim it.
|
||||
</p>
|
||||
</FullWidthRow>
|
||||
{
|
||||
projects.map(({
|
||||
projectBlockName,
|
||||
challenges,
|
||||
superBlock
|
||||
}) => {
|
||||
const isCertClaimed = blockNameIsCertMap[projectBlockName];
|
||||
if (superBlock === jsProjectSuperBlock) {
|
||||
return (
|
||||
<JSAlgoAndDSForm
|
||||
challenges={ challenges }
|
||||
claimCert={ claimCert }
|
||||
hardGoTo={ hardGoTo }
|
||||
isCertClaimed={ isCertClaimed }
|
||||
jsProjects={ userProjects[superBlock] }
|
||||
key={ superBlock }
|
||||
projectBlockName={ projectBlockName }
|
||||
superBlock={ superBlock }
|
||||
username={ username }
|
||||
/>
|
||||
);
|
||||
}
|
||||
const options = challenges
|
||||
.reduce((options, current) => {
|
||||
options.types[current] = 'url';
|
||||
return options;
|
||||
}, { types: {} });
|
||||
|
||||
options.types.id = 'hidden';
|
||||
options.placeholder = false;
|
||||
|
||||
const userValues = userProjects[superBlock] || {};
|
||||
|
||||
if (!userValues.id) {
|
||||
userValues.id = superBlock;
|
||||
}
|
||||
|
||||
const initialValues = challenges
|
||||
.reduce((accu, current) => ({
|
||||
...accu,
|
||||
[current]: ''
|
||||
}), {});
|
||||
|
||||
const completedProjects = _.values(userValues)
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
// minus 1 to account for the id
|
||||
.length - 1;
|
||||
|
||||
const fullForm = completedProjects === challenges.length;
|
||||
return (
|
||||
<FullWidthRow key={superBlock}>
|
||||
<h3 className='project-heading'>{ projectBlockName }</h3>
|
||||
<Form
|
||||
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
|
||||
enableSubmit={ fullForm }
|
||||
formFields={ challenges.concat([ 'id' ]) }
|
||||
hideButton={isCertClaimed}
|
||||
id={ superBlock }
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
...userValues
|
||||
}}
|
||||
options={ options }
|
||||
submit={ this.handleSubmit }
|
||||
/>
|
||||
{
|
||||
isCertClaimed ?
|
||||
<Button
|
||||
block={ true }
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
href={ `/c/${username}/${superBlock}`}
|
||||
>
|
||||
Show Certificate
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
<hr />
|
||||
</FullWidthRow>
|
||||
);
|
||||
})
|
||||
{
|
||||
modernProjects.map(this.buildProjectForms)
|
||||
}
|
||||
<SectionHeader>
|
||||
Legacy Certificate Settings
|
||||
</SectionHeader>
|
||||
{
|
||||
legacyProjects.map(this.buildProjectForms)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ class JSAlgoAndDSForm extends PureComponent {
|
||||
e.preventDefault();
|
||||
const { username, superBlock, isCertClaimed } = this.props;
|
||||
if (isCertClaimed) {
|
||||
return this.props.hardGoTo(`/c/${username}/${superBlock}`);
|
||||
return this.props.hardGoTo(`/certificates/${username}/${superBlock}`);
|
||||
}
|
||||
return this.props.claimCert(superBlock);
|
||||
}
|
||||
|
@ -1,19 +1,14 @@
|
||||
import { dasherize } from '../../../../../server/utils/index';
|
||||
|
||||
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
|
||||
|
||||
export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||
const {
|
||||
challengeNameIdMap,
|
||||
challenges,
|
||||
superBlock
|
||||
} = projectBlock;
|
||||
return {
|
||||
[superBlock]: challenges.reduce((solutions, current) => {
|
||||
const dashedName = dasherize(current)
|
||||
.replace('java-script', 'javascript')
|
||||
.replace('metric-imperial', 'metricimperial');
|
||||
const completed = challengeMap[challengeNameIdMap[dashedName]];
|
||||
const { id } = current;
|
||||
const completed = challengeMap[id];
|
||||
let solution = '';
|
||||
if (superBlock === jsProjectSuperBlock) {
|
||||
solution = {};
|
||||
@ -25,7 +20,7 @@ export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||
}
|
||||
return {
|
||||
...solutions,
|
||||
[current]: solution
|
||||
[current.title]: solution
|
||||
};
|
||||
}, {})
|
||||
};
|
||||
|
@ -9,7 +9,6 @@ import loopback from 'loopback';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { themes } from '../utils/themes';
|
||||
import { dasherize } from '../../server/utils';
|
||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||
@ -43,28 +42,24 @@ function destroyAll(id, Model) {
|
||||
}
|
||||
|
||||
function buildChallengeMapUpdate(challengeMap, project) {
|
||||
const key = Object.keys(project)[0];
|
||||
const solutions = project[key];
|
||||
const currentChallengeMap = { ...challengeMap };
|
||||
const { nameToIdMap } = _.values(project)[0];
|
||||
const incomingUpdate = _.pickBy(
|
||||
_.omit(_.values(project)[0], [ 'id', 'nameToIdMap' ]),
|
||||
Boolean
|
||||
const currentCompletedProjects = _.pick(
|
||||
currentChallengeMap,
|
||||
Object.keys(solutions)
|
||||
);
|
||||
const currentCompletedProjects = _.pick(challengeMap, _.values(nameToIdMap));
|
||||
const now = Date.now();
|
||||
const update = Object.keys(incomingUpdate).reduce((update, current) => {
|
||||
const dashedName = dasherize(current)
|
||||
.replace('java-script', 'javascript')
|
||||
.replace('metric-imperial', 'metricimperial');
|
||||
const currentId = nameToIdMap[dashedName];
|
||||
const update = Object.keys(solutions).reduce((update, currentId) => {
|
||||
if (
|
||||
currentId in currentCompletedProjects &&
|
||||
currentCompletedProjects[currentId].solution !== incomingUpdate[current]
|
||||
currentCompletedProjects[currentId].solution !== solutions[currentId]
|
||||
) {
|
||||
return {
|
||||
...update,
|
||||
[currentId]: {
|
||||
...currentCompletedProjects[currentId],
|
||||
solution: incomingUpdate[current],
|
||||
solution: solutions[currentId],
|
||||
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
|
||||
}
|
||||
};
|
||||
@ -74,7 +69,7 @@ function buildChallengeMapUpdate(challengeMap, project) {
|
||||
...update,
|
||||
[currentId]: {
|
||||
id: currentId,
|
||||
solution: incomingUpdate[current],
|
||||
solution: solutions[currentId],
|
||||
challengeType: 3,
|
||||
completedDate: now,
|
||||
numOfAttempts: 1
|
||||
|
88
common/utils/legacyProjectData.js
Normal file
88
common/utils/legacyProjectData.js
Normal file
@ -0,0 +1,88 @@
|
||||
const legacyFrontEndProjects = {
|
||||
challenges: [
|
||||
// build-a-personal-portfolio-webpage
|
||||
'bd7158d8c242eddfaeb5bd13',
|
||||
// build-a-random-quote-machine
|
||||
'bd7158d8c442eddfaeb5bd13',
|
||||
// build-a-pomodoro-clock
|
||||
'bd7158d8c442eddfaeb5bd0f',
|
||||
// build-a-javascript-calculator
|
||||
'bd7158d8c442eddfaeb5bd17',
|
||||
// show-the-local-weather
|
||||
'bd7158d8c442eddfaeb5bd10',
|
||||
// use-the-twitchtv-json-api
|
||||
'bd7158d8c442eddfaeb5bd1f',
|
||||
// stylize-stories-on-camper-news
|
||||
'bd7158d8c442eddfaeb5bd18',
|
||||
// build-a-wikipedia-viewer
|
||||
'bd7158d8c442eddfaeb5bd19',
|
||||
// build-a-tic-tac-toe-game
|
||||
'bd7158d8c442eedfaeb5bd1c',
|
||||
// build-a-simon-game
|
||||
'bd7158d8c442eddfaeb5bd1c'
|
||||
],
|
||||
title: 'Legacy Front End Projects',
|
||||
superBlock: 'legacy-front-end'
|
||||
};
|
||||
|
||||
const legacyBackEndProjects = {
|
||||
challenges: [
|
||||
// timestamp microservice
|
||||
'bd7158d8c443edefaeb5bdef',
|
||||
// request-header-parser-microservice
|
||||
'bd7158d8c443edefaeb5bdff',
|
||||
// url-shortener-microservice
|
||||
'bd7158d8c443edefaeb5bd0e',
|
||||
// image-search-abstraction-layer
|
||||
'bd7158d8c443edefaeb5bdee',
|
||||
// file-metadata-microservice
|
||||
'bd7158d8c443edefaeb5bd0f',
|
||||
// build-a-voting-app
|
||||
'bd7158d8c443eddfaeb5bdef',
|
||||
// build-a-nightlife-coordination-app
|
||||
'bd7158d8c443eddfaeb5bdff',
|
||||
// chart-the-stock-market
|
||||
'bd7158d8c443eddfaeb5bd0e',
|
||||
// manage-a-book-trading-club
|
||||
'bd7158d8c443eddfaeb5bd0f',
|
||||
// build-a-pinterest-clone
|
||||
'bd7158d8c443eddfaeb5bdee'
|
||||
],
|
||||
title: 'Legacy Back End Projects',
|
||||
superBlock: 'legacy-back-end'
|
||||
};
|
||||
|
||||
const legacyDataVisProjects = {
|
||||
challenges: [
|
||||
// build-a-markdown-previewer
|
||||
'bd7157d8c242eddfaeb5bd13',
|
||||
// build-a-camper-leaderboard
|
||||
'bd7156d8c242eddfaeb5bd13',
|
||||
// build-a-recipe-box
|
||||
'bd7155d8c242eddfaeb5bd13',
|
||||
// build-the-game-of-life
|
||||
'bd7154d8c242eddfaeb5bd13',
|
||||
// build-a-roguelike-dungeon-crawler-game
|
||||
'bd7153d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-bar-chart
|
||||
'bd7168d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-scatterplot-graph
|
||||
'bd7178d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-heat-map
|
||||
'bd7188d8c242eddfaeb5bd13',
|
||||
// show-national-contiguity-with-a-force-directed-graph
|
||||
'bd7198d8c242eddfaeb5bd13',
|
||||
// map-data-across-the-globe
|
||||
'bd7108d8c242eddfaeb5bd13'
|
||||
],
|
||||
title: 'Legacy Data Visualization Projects',
|
||||
superBlock: 'legacy-data-visualization'
|
||||
};
|
||||
|
||||
const legacyProjects = [
|
||||
legacyFrontEndProjects,
|
||||
legacyBackEndProjects,
|
||||
legacyDataVisProjects
|
||||
];
|
||||
|
||||
export default legacyProjects;
|
@ -273,7 +273,7 @@
|
||||
"translations": {}
|
||||
},
|
||||
{
|
||||
"id": "587d78b0367417b2b2512b06",
|
||||
"id": "bd7158d8c242eddfaeb5bd13",
|
||||
"title": "Build a Personal Portfolio Webpage",
|
||||
"description": [
|
||||
"Fulfill the user stories by getting all of the tests to pass. Use whichever libraries you need. Give it your own personal style.",
|
||||
|
@ -101,7 +101,7 @@
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd1f",
|
||||
"title": "Use the Twitch.tv JSON API",
|
||||
"title": "Use the Twitch JSON API",
|
||||
"description": [
|
||||
"<strong>Objective:</strong> Build a <a href='https://codepen.io' target='_blank'>CodePen.io</a> app that is functionally similar to this: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
|
||||
"Fulfill the below <a href='https://en.wikipedia.org/wiki/User_story' target='_blank'>user stories</a>. Use whichever libraries or APIs you need. Give it your own personal style.",
|
||||
@ -123,7 +123,7 @@
|
||||
"isRequired": false,
|
||||
"translations": {
|
||||
"es": {
|
||||
"title": "Usa el API JSON de Twitch.tv",
|
||||
"title": "Usa el API JSON de Twitch",
|
||||
"description": [
|
||||
"<span class='text-info'>Objetivo:</span> Crea una aplicación con <a href='https://codepen.io' target='_blank'>CodePen.io</a> cuya funcionalidad sea similar a la de esta: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
|
||||
"Satisface las siguientes <a href='https://en.wikipedia.org/wiki/User_story' target='_blank'>historias de usuario</a>. Usa cualquier librería o APIs que necesites. Dale tu estilo personal.",
|
||||
@ -139,7 +139,7 @@
|
||||
]
|
||||
},
|
||||
"ru": {
|
||||
"title": "Используйте Twitch.tv JSON API",
|
||||
"title": "Используйте Twitch JSON API",
|
||||
"description": [
|
||||
"<span class='text-info'>Задание:</span> Создайте <a href='https://codepen.io' target='_blank'>CodePen.io</a> который успешно копирует вот этот: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
|
||||
"<span class='text-info'>Правило #1:</span> Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.",
|
||||
@ -682,6 +682,127 @@
|
||||
"isRequired": false,
|
||||
"titleEs": "Crea un clon de Pinterest"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bdff",
|
||||
"title": "Build a Nightlife Coordination App",
|
||||
"description": [
|
||||
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='http://whatsgoinontonight.herokuapp.com/' target='_blank'>http://whatsgoinontonight.herokuapp.com/</a> and deploy it to Heroku.",
|
||||
"Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Here are the specific user stories you should implement for this project:",
|
||||
"<strong>User Story:</strong> As an unauthenticated user, I can view all bars in my area.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can add myself to a bar to indicate I am going there tonight.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can remove myself from a bar if I no longer want to go there.",
|
||||
"<strong>User Story:</strong> As an unauthenticated user, when I login I should not have to search again.",
|
||||
"<strong>Hint:</strong> Try using the <a href='https://www.yelp.com/developers/documentation/v2/overview' target='_blank'>Yelp API</a> to find venues in the cities your users search for. If you use Yelp's API, be sure to mention so in your app.",
|
||||
"Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.",
|
||||
"You can get feedback on your project by sharing it with your friends on Facebook."
|
||||
],
|
||||
"challengeSeed": [
|
||||
"Gei7QfPmcMw"
|
||||
],
|
||||
"tests": [],
|
||||
"type": "basejump",
|
||||
"challengeType": 4,
|
||||
"isRequired": true,
|
||||
"translations": {
|
||||
"es": {
|
||||
"description": [
|
||||
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='http://whatsgoinontonight.herokuapp.com/' target='_blank'>http://whatsgoinontonight.herokuapp.com/</a> y despliégala en Heroku.",
|
||||
"Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Estas son las Historias de usuario que debes satisfacer para este Basejump:",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario no autenticado, puedo ver todos los bares en mi área.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo agregarme a mí mismo a un bar para indicar que voy a estar allí esta noche.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo removerme de un bar si ya no pienso ir allí.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario no autenticado, cuando accedo a mi cuenta no debo tener la necesidad de buscar de nuevo.",
|
||||
"<span class='text-info'>Pista:</span> Prueba utilizar el <a href='https://www.yelp.com/developers/documentation/v2/overview' target='_blank'>API de Yelp</a> para encontrar lugares en las ciudades donde tus usuarios buscan. Si utilizas el API de Yelp, asegúrate de mencionarlo en tu aplicación.",
|
||||
"Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.",
|
||||
"Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bd0e",
|
||||
"title": "Chart the Stock Market",
|
||||
"description": [
|
||||
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='http://watchstocks.herokuapp.com/' target='_blank'>http://watchstocks.herokuapp.com/</a> and deploy it to Heroku.",
|
||||
"Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Here are the specific user stories you should implement for this project:",
|
||||
"<strong>User Story:</strong> I can view a graph displaying the recent trend lines for each added stock.",
|
||||
"<strong>User Story:</strong> I can add new stocks by their symbol name.",
|
||||
"<strong>User Story:</strong> I can remove stocks.",
|
||||
"<strong>User Story:</strong> I can see changes in real-time when any other user adds or removes a stock. For this you will need to use Web Sockets.",
|
||||
"Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.",
|
||||
"You can get feedback on your project by sharing it with your friends on Facebook."
|
||||
],
|
||||
"challengeSeed": [
|
||||
"CENs50cnRgM"
|
||||
],
|
||||
"tests": [],
|
||||
"type": "basejump",
|
||||
"challengeType": 4,
|
||||
"isRequired": true,
|
||||
"translations": {
|
||||
"es": {
|
||||
"description": [
|
||||
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='http://watchstocks.herokuapp.com/' target='_blank'>http://watchstocks.herokuapp.com/</a> y despliégalo en Heroku.",
|
||||
"Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Estas son las Historias de usuario que debes satisfacer para este Basejump:",
|
||||
"<strong>Historia de usuario:</strong> Como usuario, puedo ver un gráfico que me muestre las líneas de tendencia recientes para cada acción agregada.",
|
||||
"<strong>Historia de usuario:</strong> Como usuario, puedo agregar nuevas acciones por su símbolo.",
|
||||
"<strong>Historia de usuario:</strong> Como usuario, puedo remover acciones.",
|
||||
"<strong>Historia de usuario:</strong> Como usuario, puedo ver cambios en tiempo real cuando algún otro usuario agrega o remueve una acción. Puedes usar Web Sockets para hacer esto.",
|
||||
"Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.",
|
||||
"Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bdef",
|
||||
"title": "Build a Voting App",
|
||||
"description": [
|
||||
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='https://fcc-voting-arthow4n.herokuapp.com/' target='_blank'>https://fcc-voting-arthow4n.herokuapp.com/</a> and deploy it to Heroku.",
|
||||
"Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Here are the specific user stories you should implement for this project:",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can keep my polls and come back later to access them.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can share my polls with my friends.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can see the aggregate results of my polls.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can delete polls that I decide I don't want anymore.",
|
||||
"<strong>User Story:</strong> As an authenticated user, I can create a poll with any number of possible items.",
|
||||
"<strong>User Story:</strong> As an unauthenticated or authenticated user, I can see and vote on everyone's polls.",
|
||||
"<strong>User Story:</strong> As an unauthenticated or authenticated user, I can see the results of polls in chart form. (This could be implemented using Chart.js or Google Charts.)",
|
||||
"<strong>User Story:</strong> As an authenticated user, if I don't like the options on a poll, I can create a new option.",
|
||||
"Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.",
|
||||
"You can get feedback on your project by sharing it with your friends on Facebook."
|
||||
],
|
||||
"challengeSeed": [
|
||||
"JBKnbY_fdg4"
|
||||
],
|
||||
"tests": [],
|
||||
"type": "basejump",
|
||||
"challengeType": 4,
|
||||
"isRequired": true,
|
||||
"translations": {
|
||||
"es": {
|
||||
"description": [
|
||||
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='https://fcc-voting-arthow4n.herokuapp.com/' target='_blank'>https://fcc-voting-arthow4n.herokuapp.com/</a> y despliégala en Heroku.",
|
||||
"Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
|
||||
"Estas son las Historias de usuario que debes satisfacer para este proyecto:",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo guardar mis votaciones y acceder a ellas posteriormente.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo compartir mis votaciones con mis amigos.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo ver los resultados agregados de mis votaciones.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo eliminar votaciones que ya no quiero tener guardadas.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo crear una votación con cualquier número de opciones.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado o no autenticado, puedo ver y votar en las votaciones de otros.",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado o no autenticado, puedo ver los resultados de las votaciones por medio de un gráfico. (Esto podría implementarse utilizando Chart.js o Google Charts.)",
|
||||
"<strong>Historia de usuario:</strong> Como un usuario autenticado, si no me gustan las opciones en una votación, puedo crear una nueva opción.",
|
||||
"Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.",
|
||||
"Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5a4b7fcdb66f799f199e11db",
|
||||
"title": "Build a Pong Game",
|
||||
|
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Legacy Back End Certificate",
|
||||
"order": 1,
|
||||
"isPrivate": true,
|
||||
"challenges": [
|
||||
{
|
||||
"id": "660add10cb82ac38a17513be",
|
||||
"title": "Legacy Back End Certificate",
|
||||
"challengeType": 7,
|
||||
"description": [],
|
||||
"challengeSeed": [],
|
||||
"isPrivate": true,
|
||||
"tests": [
|
||||
{
|
||||
"id": "bd7158d8c443edefaeb5bdef",
|
||||
"title": "Timestamp Microservice"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443edefaeb5bdff",
|
||||
"title": "Request Header Parser Microservice"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443edefaeb5bd0e",
|
||||
"title": "URL Shortener Microservice"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443edefaeb5bdee",
|
||||
"title": "Image Search Abstraction Layer"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443edefaeb5bd0f",
|
||||
"title": "File Metadata Microservice"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bdef",
|
||||
"title": "Build a Voting App"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bdff",
|
||||
"title": "Build a Nightlife Coordination App"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bd0e",
|
||||
"title": "Chart the Stock Market"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bd0f",
|
||||
"title": "Manage a Book Trading Club"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c443eddfaeb5bdee",
|
||||
"title": "Build a Pinterest Clone"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Legacy Data Visualization Certificate",
|
||||
"order": 1,
|
||||
"isPrivate": true,
|
||||
"challenges": [
|
||||
{
|
||||
"id": "561add10cb82ac39a17513bc",
|
||||
"title": "Legacy Data Visualization Certificate",
|
||||
"challengeType": 7,
|
||||
"description": [],
|
||||
"challengeSeed": [],
|
||||
"isPrivate": true,
|
||||
"tests": [
|
||||
{
|
||||
"id": "bd7157d8c242eddfaeb5bd13",
|
||||
"title": "Build a Markdown Previewer"
|
||||
},
|
||||
{
|
||||
"id": "bd7156d8c242eddfaeb5bd13",
|
||||
"title": "Build a Camper Leaderboard"
|
||||
},
|
||||
{
|
||||
"id": "bd7155d8c242eddfaeb5bd13",
|
||||
"title": "Build a Recipe Box"
|
||||
},
|
||||
{
|
||||
"id": "bd7154d8c242eddfaeb5bd13",
|
||||
"title": "Build the Game of Life"
|
||||
},
|
||||
{
|
||||
"id": "bd7153d8c242eddfaeb5bd13",
|
||||
"title": "Build a Roguelike Dungeon Crawler Game"
|
||||
},
|
||||
{
|
||||
"id": "bd7168d8c242eddfaeb5bd13",
|
||||
"title": "Visualize Data with a Bar Chart"
|
||||
},
|
||||
{
|
||||
"id": "bd7178d8c242eddfaeb5bd13",
|
||||
"title": "Visualize Data with a Scatterplot Graph"
|
||||
},
|
||||
{
|
||||
"id": "bd7188d8c242eddfaeb5bd13",
|
||||
"title": "Visualize Data with a Heat Map"
|
||||
},
|
||||
{
|
||||
"id": "bd7198d8c242eddfaeb5bd13",
|
||||
"title": "Show National Contiguity with a Force Directed Graph"
|
||||
},
|
||||
{
|
||||
"id": "bd7108d8c242eddfaeb5bd13",
|
||||
"title": "Map Data Across the Globe"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Legacy Front End Certificate",
|
||||
"order": 1,
|
||||
"isPrivate": true,
|
||||
"challenges": [
|
||||
{
|
||||
"id": "561add10cb82ac38a17513be",
|
||||
"title": "Legacy Front End Certificate",
|
||||
"challengeType": 7,
|
||||
"description": [],
|
||||
"challengeSeed": [],
|
||||
"isPrivate": true,
|
||||
"tests": [
|
||||
{
|
||||
"id": "bd7158d8c242eddfaeb5bd13",
|
||||
"title": "Build a Personal Portfolio Webpage"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd13",
|
||||
"title": "Build a Random Quote Machine"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd0f",
|
||||
"title": "Build a Pomodoro Clock"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd17",
|
||||
"title": "Build a JavaScript Calculator"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd10",
|
||||
"title": "Show the Local Weather"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd1f",
|
||||
"title": "Use the Twitch JSON API"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd18",
|
||||
"title": "Stylize Stories on Camper News"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd19",
|
||||
"title": "Build a Wikipedia Viewer"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eedfaeb5bd1c",
|
||||
"title": "Build a Tic Tac Toe Game"
|
||||
},
|
||||
{
|
||||
"id": "bd7158d8c442eddfaeb5bd1c",
|
||||
"title": "Build a Simon Game"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import loopback from 'loopback';
|
||||
import moment from 'moment-timezone';
|
||||
import path from 'path';
|
||||
import dedent from 'dedent';
|
||||
import { Observable } from 'rx';
|
||||
@ -13,26 +14,23 @@ import {
|
||||
import { observeQuery } from '../utils/rx';
|
||||
|
||||
import {
|
||||
// legacy
|
||||
frontEndChallengeId,
|
||||
backEndChallengeId,
|
||||
dataVisId,
|
||||
legacyFrontEndChallengeId,
|
||||
legacyBackEndChallengeId,
|
||||
legacyDataVisId,
|
||||
|
||||
// modern
|
||||
respWebDesignId,
|
||||
frontEndLibsId,
|
||||
dataVis2018Id,
|
||||
jsAlgoDataStructId,
|
||||
dataVis2018Id,
|
||||
apisMicroservicesId,
|
||||
infosecQaId
|
||||
} from '../utils/constantStrings.json';
|
||||
import certTypes from '../utils/certTypes.json';
|
||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||
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(
|
||||
__dirname,
|
||||
@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
|
||||
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
||||
}
|
||||
|
||||
const certIds = {
|
||||
[certTypes.frontEnd]: legacyFrontEndChallengeId,
|
||||
[certTypes.backEnd]: legacyBackEndChallengeId,
|
||||
[certTypes.dataVis]: legacyDataVisId,
|
||||
[certTypes.respWebDesign]: respWebDesignId,
|
||||
[certTypes.frontEndLibs]: frontEndLibsId,
|
||||
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
||||
[certTypes.dataVis2018]: dataVis2018Id,
|
||||
[certTypes.apisMicroservices]: apisMicroservicesId,
|
||||
[certTypes.infosecQa]: infosecQaId
|
||||
};
|
||||
|
||||
const certViews = {
|
||||
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
|
||||
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
|
||||
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
|
||||
[certTypes.fullStack]: 'certificate/legacy/full-stack.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'
|
||||
};
|
||||
|
||||
const certText = {
|
||||
[certTypes.frontEnd]: 'Legacy Front End certified',
|
||||
[certTypes.backEnd]: 'Legacy Back End Certified',
|
||||
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
|
||||
[certTypes.fullStack]: 'Legacy Full Stack Certified',
|
||||
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
|
||||
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
|
||||
[certTypes.jsAlgoDataStruct]:
|
||||
'JavaScript Algorithms and Data Structures Certified',
|
||||
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
||||
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
||||
};
|
||||
|
||||
function getIdsForCert$(id, Challenge) {
|
||||
return observeQuery(
|
||||
Challenge,
|
||||
@ -117,13 +157,24 @@ function sendCertifiedEmail(
|
||||
|
||||
export default function certificate(app) {
|
||||
const router = app.loopback.Router();
|
||||
const { Email, Challenge } = app.models;
|
||||
const { Email, Challenge, User } = app.models;
|
||||
|
||||
function findUserByUsername$(username, fields) {
|
||||
return observeQuery(
|
||||
User,
|
||||
'findOne',
|
||||
{
|
||||
where: { username },
|
||||
fields
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const certTypeIds = {
|
||||
// legacy
|
||||
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
|
||||
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
|
||||
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
||||
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
|
||||
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
|
||||
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
|
||||
|
||||
// modern
|
||||
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
||||
@ -145,6 +196,10 @@ export default function certificate(app) {
|
||||
ifNoSuperBlock404,
|
||||
verifyCert
|
||||
);
|
||||
router.get(
|
||||
'/certificates/:username/:cert',
|
||||
showCert
|
||||
);
|
||||
|
||||
app.use(router);
|
||||
|
||||
@ -172,16 +227,12 @@ export default function certificate(app) {
|
||||
|
||||
function verifyCert(req, res, next) {
|
||||
const { body: { superBlock }, user } = req;
|
||||
|
||||
log(superBlock);
|
||||
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,
|
||||
@ -251,4 +302,101 @@ export default function certificate(app) {
|
||||
}
|
||||
return res.status(404).end();
|
||||
}
|
||||
|
||||
function showCert(req, res, next) {
|
||||
let { username, cert } = req.params;
|
||||
username = username.toLowerCase();
|
||||
const certType = superBlockCertTypeMap[cert];
|
||||
const certId = certIds[certType];
|
||||
return findUserByUsername$(
|
||||
username,
|
||||
{
|
||||
isCheater: true,
|
||||
isLocked: true,
|
||||
isFrontEndCert: true,
|
||||
isBackEndCert: true,
|
||||
isFullStackCert: true,
|
||||
isRespWebDesignCert: true,
|
||||
isFrontEndLibsCert: true,
|
||||
isJsAlgoDataStructCert: true,
|
||||
isDataVisCert: true,
|
||||
is2018DataVisCert: true,
|
||||
isApisMicroservicesCert: true,
|
||||
isInfosecQaCert: true,
|
||||
isHonest: true,
|
||||
username: true,
|
||||
name: true,
|
||||
challengeMap: true
|
||||
}
|
||||
)
|
||||
.subscribe(
|
||||
user => {
|
||||
const profile = `/${user.username}`;
|
||||
if (!user) {
|
||||
req.flash(
|
||||
'danger',
|
||||
`We couldn't find a user with the username ${username}`
|
||||
);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
This user needs to add their name to their account
|
||||
in order for others to be able to view their certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user.isCheater) {
|
||||
return res.redirect(`/${user.username}`);
|
||||
}
|
||||
|
||||
if (user.isLocked) {
|
||||
req.flash(
|
||||
'danger',
|
||||
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 certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!user.isHonest) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
${username} has not yet agreed to our Academic Honesty Pledge.
|
||||
`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user[certType]) {
|
||||
const { challengeMap = {} } = user;
|
||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||
|
||||
return res.render(
|
||||
certViews[certType],
|
||||
{
|
||||
username: user.username,
|
||||
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
|
||||
name: user.name
|
||||
}
|
||||
);
|
||||
}
|
||||
req.flash(
|
||||
'danger',
|
||||
`Looks like user ${username} is not ${certText[certType]}`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,88 +1,22 @@
|
||||
import dedent from 'dedent';
|
||||
import moment from 'moment-timezone';
|
||||
import debugFactory from 'debug';
|
||||
import { curry } from 'lodash';
|
||||
|
||||
import {
|
||||
frontEndChallengeId,
|
||||
backEndChallengeId,
|
||||
respWebDesignId,
|
||||
frontEndLibsId,
|
||||
jsAlgoDataStructId,
|
||||
dataVisId,
|
||||
dataVis2018Id,
|
||||
apisMicroservicesId,
|
||||
infosecQaId
|
||||
} from '../utils/constantStrings.json';
|
||||
import certTypes from '../utils/certTypes.json';
|
||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||
import {
|
||||
ifNoUser401,
|
||||
ifNoUserRedirectTo,
|
||||
ifNotVerifiedRedirectToSettings
|
||||
} from '../utils/middleware';
|
||||
import { observeQuery } from '../utils/rx';
|
||||
|
||||
const debug = debugFactory('fcc:boot:user');
|
||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
||||
const certIds = {
|
||||
[certTypes.frontEnd]: frontEndChallengeId,
|
||||
[certTypes.backEnd]: backEndChallengeId,
|
||||
[certTypes.respWebDesign]: respWebDesignId,
|
||||
[certTypes.frontEndLibs]: frontEndLibsId,
|
||||
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
||||
[certTypes.dataVis]: dataVisId,
|
||||
[certTypes.dataVis2018]: dataVis2018Id,
|
||||
[certTypes.apisMicroservices]: apisMicroservicesId,
|
||||
[certTypes.infosecQa]: infosecQaId
|
||||
};
|
||||
|
||||
const certViews = {
|
||||
[certTypes.frontEnd]: 'certificate/front-end.jade',
|
||||
[certTypes.backEnd]: 'certificate/back-end.jade',
|
||||
[certTypes.fullStack]: 'certificate/full-stack.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.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'
|
||||
};
|
||||
|
||||
const certText = {
|
||||
[certTypes.frontEnd]: 'Front End certified',
|
||||
[certTypes.backEnd]: 'Back End Certified',
|
||||
[certTypes.fullStack]: 'Full Stack Certified',
|
||||
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
|
||||
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
|
||||
[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'
|
||||
};
|
||||
|
||||
module.exports = function(app) {
|
||||
const router = app.loopback.Router();
|
||||
const api = app.loopback.Router();
|
||||
const { Email, User } = app.models;
|
||||
|
||||
function findUserByUsername$(username, fields) {
|
||||
return observeQuery(
|
||||
User,
|
||||
'findOne',
|
||||
{
|
||||
where: { username },
|
||||
fields
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/account/delete',
|
||||
ifNoUser401,
|
||||
@ -105,11 +39,6 @@ module.exports = function(app) {
|
||||
);
|
||||
|
||||
// Ensure these are the last routes!
|
||||
api.get(
|
||||
'/c/:username/:cert',
|
||||
showCert
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/user/:username/report-user/',
|
||||
sendNonUserToMapWithMessage('You must be signed in to report a user'),
|
||||
@ -185,100 +114,6 @@ module.exports = function(app) {
|
||||
});
|
||||
}
|
||||
|
||||
function showCert(req, res, next) {
|
||||
let { username, cert } = req.params;
|
||||
username = username.toLowerCase();
|
||||
const certType = superBlockCertTypeMap[cert];
|
||||
const certId = certIds[certType];
|
||||
return findUserByUsername$(username, {
|
||||
isCheater: true,
|
||||
isLocked: true,
|
||||
isFrontEndCert: true,
|
||||
isBackEndCert: true,
|
||||
isFullStackCert: true,
|
||||
isRespWebDesignCert: true,
|
||||
isFrontEndLibsCert: true,
|
||||
isJsAlgoDataStructCert: true,
|
||||
isDataVisCert: true,
|
||||
is2018DataVisCert: true,
|
||||
isApisMicroservicesCert: true,
|
||||
isInfosecQaCert: true,
|
||||
isHonest: true,
|
||||
username: true,
|
||||
name: true,
|
||||
challengeMap: true
|
||||
})
|
||||
.subscribe(
|
||||
user => {
|
||||
const profile = `/${user.username}`;
|
||||
if (!user) {
|
||||
req.flash(
|
||||
'danger',
|
||||
`We couldn't find a user with the username ${username}`
|
||||
);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
This user needs to add their name to their account
|
||||
in order for others to be able to view their certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user.isCheater) {
|
||||
return res.redirect(`/${user.username}`);
|
||||
}
|
||||
|
||||
if (user.isLocked) {
|
||||
req.flash(
|
||||
'danger',
|
||||
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 certificate.
|
||||
`
|
||||
);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!user.isHonest) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
${username} has not yet agreed to our Academic Honesty Pledge.
|
||||
`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user[certType]) {
|
||||
const { challengeMap = {} } = user;
|
||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||
|
||||
return res.render(
|
||||
certViews[certType],
|
||||
{
|
||||
username: user.username,
|
||||
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
|
||||
name: user.name
|
||||
}
|
||||
);
|
||||
}
|
||||
req.flash(
|
||||
'danger',
|
||||
`Looks like user ${username} is not ${certText[certType]}`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function postDeleteAccount(req, res, next) {
|
||||
User.destroyById(req.user.id, function(err) {
|
||||
if (err) { return next(err); }
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"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",
|
||||
"legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
|
||||
"legacyBackEndChallengeId": "660add10cb82ac38a17513be",
|
||||
"legacyDataVisId": "561add10cb82ac39a17513bc",
|
||||
|
||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||
|
@ -15,6 +15,7 @@ export const publicUserProps = [
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'is2018DataVisCert',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
|
@ -2,16 +2,16 @@ import certTypes from './certTypes.json';
|
||||
|
||||
const superBlockCertTypeMap = {
|
||||
// legacy
|
||||
'front-end': certTypes.frontEnd,
|
||||
'back-end': certTypes.backEnd,
|
||||
'data-visualization': certTypes.dataVis,
|
||||
'full-stack': certTypes.fullStack,
|
||||
'legacy-front-end': certTypes.frontEnd,
|
||||
'legacy-back-end': certTypes.backEnd,
|
||||
'legacy-data-visualization': certTypes.dataVis,
|
||||
'legacy-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,
|
||||
'data-visualization': certTypes.dataVis2018,
|
||||
'apis-and-microservices': certTypes.apisMicroservices,
|
||||
'information-security-and-quality-assurance': certTypes.infosecQa
|
||||
};
|
||||
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/advanced-front-end-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/advanced-front-end
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/apis-and-microservices-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/apis-and-microservices
|
||||
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/data-visualization-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/data-visualization
|
||||
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-libraries-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/front-end-libraries
|
||||
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/information-security-and-quality-assurance-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/information-security-and-quality-assurance
|
||||
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/javascript-algorithms-and-data-structures-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/javascript-algorithms-and-data-structures
|
||||
|
@ -1,6 +1,6 @@
|
||||
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
|
||||
include ../styles
|
||||
|
||||
.certificate-wrapper.container
|
||||
.row
|
||||
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/back-end-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-back-end
|
32
server/views/certificate/legacy/data-visualization.jade
Normal file
32
server/views/certificate/legacy/data-visualization.jade
Normal file
@ -0,0 +1,32 @@
|
||||
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 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")
|
||||
p
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-data-visualization
|
@ -1,6 +1,6 @@
|
||||
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
|
||||
include ../styles
|
||||
|
||||
.certificate-wrapper.container
|
||||
.row
|
||||
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-front-end
|
@ -1,6 +1,6 @@
|
||||
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
|
||||
include ../styles
|
||||
|
||||
.certificate-wrapper.container
|
||||
.row
|
||||
@ -19,8 +19,8 @@ include styles
|
||||
strong= name
|
||||
h3 has successfully completed freeCodeCamp's
|
||||
h1
|
||||
strong Full Stack Development Projects
|
||||
h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework
|
||||
strong Legacy Full Stack Development Program
|
||||
h4 All three of the legacy freeCodeCamp certificates, representing approximately 12000 hours of coursework
|
||||
|
||||
footer
|
||||
.row.signatures
|
||||
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/full-stack-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-full-stack
|
@ -29,4 +29,4 @@ include styles
|
||||
strong Quincy Larson
|
||||
p Executive Director, freeCodeCamp.org
|
||||
.row
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/responsive-web-design-certification
|
||||
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/responsive-web-design
|
||||
|
Reference in New Issue
Block a user