feat(legacy-certs): Claim legacy certificates from the settings page
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import { values as _values, isString, findIndex } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
@ -12,11 +12,17 @@ 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
|
||||
} from '../utils/buildUserProjectsMap';
|
||||
import legacyProjects from '../utils/legacyProjectData';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
@ -30,12 +36,15 @@ const mapStateToProps = createSelector(
|
||||
isJsAlgoDataStructCert,
|
||||
isApisMicroservicesCert,
|
||||
isInfosecQaCert,
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isDataVisCert,
|
||||
username
|
||||
},
|
||||
projects
|
||||
) => ({
|
||||
projects,
|
||||
userProjects: projects
|
||||
userProjects: projects.concat(legacyProjects)
|
||||
.map(block => buildUserProjectsMap(block, challengeMap))
|
||||
.reduce((projects, current) => ({
|
||||
...projects,
|
||||
@ -49,7 +58,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,6 +70,7 @@ const mapStateToProps = createSelector(
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({
|
||||
claimCert,
|
||||
createError: createErrorObservable,
|
||||
fetchChallenges,
|
||||
hardGoTo,
|
||||
updateUserBackend
|
||||
@ -67,6 +80,7 @@ function mapDispatchToProps(dispatch) {
|
||||
const propTypes = {
|
||||
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
|
||||
claimCert: PropTypes.func.isRequired,
|
||||
createError: PropTypes.func.isRequired,
|
||||
fetchChallenges: PropTypes.func.isRequired,
|
||||
hardGoTo: PropTypes.func.isRequired,
|
||||
projects: PropTypes.arrayOf(
|
||||
@ -88,11 +102,15 @@ const propTypes = {
|
||||
username: PropTypes.string
|
||||
};
|
||||
|
||||
const compareSuperBlockWith = id => p => p.superBlock === id;
|
||||
|
||||
class CertificationSettings extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.buildProjectForms = this.buildProjectForms.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.isProjectSectionCompleted = this.isProjectSectionCompleted.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -102,23 +120,154 @@ class CertificationSettings extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
buildProjectForms({
|
||||
projectBlockName,
|
||||
challenges,
|
||||
superBlock
|
||||
}) {
|
||||
const {
|
||||
blockNameIsCertMap,
|
||||
claimCert,
|
||||
hardGoTo,
|
||||
userProjects,
|
||||
username
|
||||
} = this.props;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
isProjectSectionCompleted(values) {
|
||||
const { id } = values;
|
||||
const { projects } = this.props;
|
||||
const whereSuperBlockIsId = compareSuperBlockWith(id);
|
||||
|
||||
let pIndex = findIndex(projects, whereSuperBlockIsId);
|
||||
let projectChallenges = [];
|
||||
|
||||
if (pIndex === -1) {
|
||||
// submitted projects might be in a legacy certificate
|
||||
pIndex = findIndex(legacyProjects, whereSuperBlockIsId);
|
||||
projectChallenges = legacyProjects[pIndex].challenges;
|
||||
if (pIndex === -1) {
|
||||
// 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'
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
projectChallenges = projects[pIndex].challenges;
|
||||
}
|
||||
const valuesSaved = _values(this.props.userProjects[id])
|
||||
.filter(Boolean)
|
||||
.filter(isString);
|
||||
// minus 1 due to the form id being in values
|
||||
return (valuesSaved.length - 1) === projectChallenges.length;
|
||||
}
|
||||
|
||||
handleSubmit(values) {
|
||||
const { id } = values;
|
||||
const fullForm = _.values(values)
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
// 5 projects + 1 id prop
|
||||
.length === 6;
|
||||
const valuesSaved = _.values(this.props.userProjects[id])
|
||||
.filter(Boolean)
|
||||
.filter(_.isString)
|
||||
.length === 6;
|
||||
if (fullForm && valuesSaved) {
|
||||
if (this.isProjectSectionCompleted(values)) {
|
||||
return this.props.claimCert(id);
|
||||
}
|
||||
const { projects } = this.props;
|
||||
const pIndex = _.findIndex(projects, p => p.superBlock === id);
|
||||
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
|
||||
const whereSuperBlockIsId = compareSuperBlockWith(id);
|
||||
|
||||
let pIndex = findIndex(projects, whereSuperBlockIsId);
|
||||
let projectNameIdMap = {};
|
||||
|
||||
if (pIndex === -1) {
|
||||
// submitted projects might be in a legacy certificate
|
||||
pIndex = findIndex(legacyProjects, whereSuperBlockIsId);
|
||||
projectNameIdMap = legacyProjects[pIndex].challengeNameIdMap;
|
||||
if (pIndex === -1) {
|
||||
// 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'
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
projectNameIdMap = projects[pIndex].challengeNameIdMap;
|
||||
}
|
||||
values.nameToIdMap = projectNameIdMap;
|
||||
return this.props.updateUserBackend({
|
||||
projects: {
|
||||
[id]: values
|
||||
@ -128,12 +277,7 @@ class CertificationSettings extends PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
blockNameIsCertMap,
|
||||
claimCert,
|
||||
hardGoTo,
|
||||
projects,
|
||||
userProjects,
|
||||
username
|
||||
projects
|
||||
} = this.props;
|
||||
if (!projects.length) {
|
||||
return null;
|
||||
@ -150,88 +294,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>
|
||||
);
|
||||
})
|
||||
{
|
||||
projects.map(this.buildProjectForms)
|
||||
}
|
||||
<SectionHeader>
|
||||
Legacy Certificate Settings
|
||||
</SectionHeader>
|
||||
{
|
||||
legacyProjects.map(this.buildProjectForms)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
95
common/app/routes/Settings/utils/legacyProjectData.js
Normal file
95
common/app/routes/Settings/utils/legacyProjectData.js
Normal file
@ -0,0 +1,95 @@
|
||||
const legacyFrontEndProjects = {
|
||||
challengeNameIdMap: {
|
||||
'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'
|
||||
},
|
||||
challenges: [
|
||||
'Build a Personal Portfolio Webpage',
|
||||
'Build a Random Quote Machine',
|
||||
'Build a Pomodoro Clock',
|
||||
'Build a JavaScript Calculator',
|
||||
'Show the Local Weather',
|
||||
'Use the Twitchtv JSON API',
|
||||
'Stylize Stories on Camper News',
|
||||
'Build a Wikipedia Viewer',
|
||||
'Build a Tic Tac Toe Game',
|
||||
'Build a Simon Game'
|
||||
],
|
||||
projectBlockName: 'Legacy Front End Projects',
|
||||
superBlock: 'legacy-front-end'
|
||||
};
|
||||
|
||||
const legacyBackEndProjects = {
|
||||
challengeNameIdMap: {
|
||||
'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'
|
||||
},
|
||||
challenges: [
|
||||
'Timestamp Microservice',
|
||||
'Request Header Parser Microservice',
|
||||
'URL Shortener Microservice',
|
||||
'Image Search Abstraction Layer',
|
||||
'File Metadata Microservice',
|
||||
'Build a Voting App',
|
||||
'Build a Nightlife Coordination App',
|
||||
'Chart the Stock Market',
|
||||
'Manage a Book Trading Club',
|
||||
'Build a Pinterest Clone'
|
||||
],
|
||||
projectBlockName: 'Legacy Back End Projects',
|
||||
superBlock: 'legacy-back-end'
|
||||
};
|
||||
|
||||
const legacyDataVisProjects = {
|
||||
challengeNameIdMap: {
|
||||
'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'
|
||||
},
|
||||
challenges: [
|
||||
'Build a Markdown Previewer',
|
||||
'Build a Camper Leaderboard',
|
||||
'Build a Recipe Box',
|
||||
'Build the Game of Life',
|
||||
'Build a Roguelike Dungeon Crawler Game',
|
||||
'Visualize Data with a Bar Chart',
|
||||
'Visualize Data with a Scatterplot Graph',
|
||||
'Visualize Data with a Heat Map',
|
||||
'Show National Contiguity with a Force Directed Graph',
|
||||
'Map Data Across the Globe'
|
||||
],
|
||||
projectBlockName: 'Legacy Data Visualization Projects',
|
||||
superBlock: 'legacy-data-visualization'
|
||||
};
|
||||
|
||||
const legacyProjects = [
|
||||
legacyFrontEndProjects,
|
||||
legacyBackEndProjects,
|
||||
legacyDataVisProjects
|
||||
];
|
||||
|
||||
export default legacyProjects;
|
@ -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.tv 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(
|
||||
'/c/: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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{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/c/#{username}/responsive-web-design
|
||||
|
Reference in New Issue
Block a user