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 uuid from 'uuid/v4'; | ||||||
| import { | import { | ||||||
|   combineActions, |   combineActions, | ||||||
| @@ -12,6 +12,7 @@ import { themes } from '../../utils/themes'; | |||||||
| import { usernameSelector, types as app } from '../redux'; | import { usernameSelector, types as app } from '../redux'; | ||||||
| import { types as challenges } from '../routes/Challenges/redux'; | import { types as challenges } from '../routes/Challenges/redux'; | ||||||
| import { types as map } from '../Map/redux'; | import { types as map } from '../Map/redux'; | ||||||
|  | import legacyProjects from '../../utils/legacyProjectData'; | ||||||
|  |  | ||||||
| export const ns = 'entities'; | export const ns = 'entities'; | ||||||
| export const getNS = state => state[ns]; | export const getNS = state => state[ns]; | ||||||
| @@ -91,10 +92,6 @@ const defaultState = { | |||||||
|   fullBlocks: [] |   fullBlocks: [] | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export function selectiveChallengeTitleSelector(state, dashedName) { |  | ||||||
|   return getNS(state).challenge[dashedName].title; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function portfolioSelector(state, props) { | export function portfolioSelector(state, props) { | ||||||
|   const username = usernameSelector(state); |   const username = usernameSelector(state); | ||||||
|   const { portfolio } = getNS(state).user[username]; |   const { portfolio } = getNS(state).user[username]; | ||||||
| @@ -103,27 +100,42 @@ export function portfolioSelector(state, props) { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function projectsSelector(state) { | export function projectsSelector(state) { | ||||||
|   const blocks = getNS(state).block; |   const { | ||||||
|   const challengeNameToIdMap = invert(challengeIdToNameMapSelector(state)); |     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) |   return Object.keys(blocks) | ||||||
|     .filter(key => |     .filter(key => | ||||||
|       key.includes('projects') && !key.includes('coding-interview') |       key.includes('projects') && !key.includes('coding-interview') | ||||||
|     ) |     ) | ||||||
|     .map(key => blocks[key]) |     .map(key => blocks[key]) | ||||||
|  |     .concat(legacyWithDashedNames) | ||||||
|     .map(({ title, challenges, superBlock }) => { |     .map(({ title, challenges, superBlock }) => { | ||||||
|       const projectChallengeDashNames = challenges |       const projectChallengeDashNames = challenges | ||||||
|  |         // challengeIdToName is not available on appMount | ||||||
|  |         .filter(Boolean) | ||||||
|         // remove any project intros |         // remove any project intros | ||||||
|         .filter(chal => !chal.includes('get-set-for')); |         .filter(chal => !chal.includes('get-set-for')); | ||||||
|       const projectChallenges = projectChallengeDashNames |       const projectChallenges = projectChallengeDashNames | ||||||
|         .map(dashedName => selectiveChallengeTitleSelector(state, dashedName)); |         .map(dashedName => { | ||||||
|  |           const { id, title } = challengeMap[dashedName]; | ||||||
|  |           return { id, title, dashedName }; | ||||||
|  |         }); | ||||||
|       return { |       return { | ||||||
|         projectBlockName: title, |         projectBlockName: title, | ||||||
|         superBlock, |         superBlock, | ||||||
|         challenges: projectChallenges, |         challenges: projectChallenges | ||||||
|         challengeNameIdMap: pick( |  | ||||||
|           challengeNameToIdMap, |  | ||||||
|           projectChallengeDashNames |  | ||||||
|         ) |  | ||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ const mapStateToProps = createSelector( | |||||||
|       { |       { | ||||||
|         show: is2018DataVisCert, |         show: is2018DataVisCert, | ||||||
|         title: 'Data Visualization Certificate:', |         title: 'Data Visualization Certificate:', | ||||||
|         showURL: 'data-visualization-2018' |         showURL: 'data-visualization' | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         show: isApisMicroservicesCert, |         show: isApisMicroservicesCert, | ||||||
| @@ -78,22 +78,22 @@ const mapStateToProps = createSelector( | |||||||
|       { |       { | ||||||
|         show: isFullStackCert, |         show: isFullStackCert, | ||||||
|         title: 'Full Stack Certificate:', |         title: 'Full Stack Certificate:', | ||||||
|         showURL: 'full-stack' |         showURL: 'legacy-full-stack' | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         show: isFrontEndCert, |         show: isFrontEndCert, | ||||||
|         title: 'Front End Certificate:', |         title: 'Front End Certificate:', | ||||||
|         showURL: 'front-end' |         showURL: 'legacy-front-end' | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         show: isBackEndCert, |         show: isBackEndCert, | ||||||
|         title: 'Back End Certificate:', |         title: 'Back End Certificate:', | ||||||
|         showURL: 'back-end' |         showURL: 'legacy-back-end' | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         show: isDataVisCert, |         show: isDataVisCert, | ||||||
|         title: 'Data Visualization Certificate:', |         title: 'Data Visualization Certificate:', | ||||||
|         showURL: 'data-visualization' |         showURL: 'legacy-data-visualization' | ||||||
|       } |       } | ||||||
|     ] |     ] | ||||||
|   }) |   }) | ||||||
| @@ -134,7 +134,7 @@ function renderCertShow(username, cert) { | |||||||
|           block={ true } |           block={ true } | ||||||
|           bsSize='lg' |           bsSize='lg' | ||||||
|           bsStyle='primary' |           bsStyle='primary' | ||||||
|           href={ `/c/${username}/${cert.showURL}`} |           href={ `/certificates/${username}/${cert.showURL}`} | ||||||
|           > |           > | ||||||
|           Show |           Show | ||||||
|         </Button> |         </Button> | ||||||
|   | |||||||
| @@ -12,7 +12,12 @@ import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx'; | |||||||
| import SectionHeader from './SectionHeader.jsx'; | import SectionHeader from './SectionHeader.jsx'; | ||||||
| import { projectsSelector } from '../../../entities'; | import { projectsSelector } from '../../../entities'; | ||||||
| import { claimCert, updateUserBackend } from '../redux'; | import { claimCert, updateUserBackend } from '../redux'; | ||||||
| import { fetchChallenges, userSelector, hardGoTo } from '../../../redux'; | import { | ||||||
|  |   fetchChallenges, | ||||||
|  |   userSelector, | ||||||
|  |   hardGoTo, | ||||||
|  |   createErrorObservable | ||||||
|  | } from '../../../redux'; | ||||||
| import { | import { | ||||||
|   buildUserProjectsMap, |   buildUserProjectsMap, | ||||||
|   jsProjectSuperBlock |   jsProjectSuperBlock | ||||||
| @@ -30,11 +35,16 @@ const mapStateToProps = createSelector( | |||||||
|       isJsAlgoDataStructCert, |       isJsAlgoDataStructCert, | ||||||
|       isApisMicroservicesCert, |       isApisMicroservicesCert, | ||||||
|       isInfosecQaCert, |       isInfosecQaCert, | ||||||
|  |       isFrontEndCert, | ||||||
|  |       isBackEndCert, | ||||||
|  |       isDataVisCert, | ||||||
|       username |       username | ||||||
|     }, |     }, | ||||||
|     projects |     projects | ||||||
|   ) => ({ |   ) => ({ | ||||||
|     projects, |     allProjects: projects, | ||||||
|  |     legacyProjects: projects.filter(p => p.superBlock.includes('legacy')), | ||||||
|  |     modernProjects: projects.filter(p => !p.superBlock.includes('legacy')), | ||||||
|     userProjects: projects |     userProjects: projects | ||||||
|       .map(block => buildUserProjectsMap(block, challengeMap)) |       .map(block => buildUserProjectsMap(block, challengeMap)) | ||||||
|       .reduce((projects, current) => ({ |       .reduce((projects, current) => ({ | ||||||
| @@ -49,7 +59,10 @@ const mapStateToProps = createSelector( | |||||||
|       'Front End Libraries Projects': isFrontEndLibsCert, |       'Front End Libraries Projects': isFrontEndLibsCert, | ||||||
|       'Data Visualization Projects': is2018DataVisCert, |       'Data Visualization Projects': is2018DataVisCert, | ||||||
|       'API and Microservice Projects': isApisMicroservicesCert, |       '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 |     username | ||||||
|   }) |   }) | ||||||
| @@ -58,23 +71,35 @@ const mapStateToProps = createSelector( | |||||||
| function mapDispatchToProps(dispatch) { | function mapDispatchToProps(dispatch) { | ||||||
|   return bindActionCreators({ |   return bindActionCreators({ | ||||||
|     claimCert, |     claimCert, | ||||||
|  |     createError: createErrorObservable, | ||||||
|     fetchChallenges, |     fetchChallenges, | ||||||
|     hardGoTo, |     hardGoTo, | ||||||
|     updateUserBackend |     updateUserBackend | ||||||
|   }, dispatch); |   }, dispatch); | ||||||
| } | } | ||||||
|  |  | ||||||
| const propTypes = { | const projectsTypes = PropTypes.arrayOf( | ||||||
|   blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool), |  | ||||||
|   claimCert: PropTypes.func.isRequired, |  | ||||||
|   fetchChallenges: PropTypes.func.isRequired, |  | ||||||
|   hardGoTo: PropTypes.func.isRequired, |  | ||||||
|   projects: PropTypes.arrayOf( |  | ||||||
|   PropTypes.shape({ |   PropTypes.shape({ | ||||||
|     projectBlockName: PropTypes.string, |     projectBlockName: PropTypes.string, | ||||||
|       challenges: PropTypes.arrayOf(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, | ||||||
|  |   legacyProjects: projectsTypes, | ||||||
|  |   modernProjects: projectsTypes, | ||||||
|   superBlock: PropTypes.string, |   superBlock: PropTypes.string, | ||||||
|   updateUserBackend: PropTypes.func.isRequired, |   updateUserBackend: PropTypes.func.isRequired, | ||||||
|   userProjects: PropTypes.objectOf( |   userProjects: PropTypes.objectOf( | ||||||
| @@ -92,75 +117,36 @@ class CertificationSettings extends PureComponent { | |||||||
|   constructor(props) { |   constructor(props) { | ||||||
|     super(props); |     super(props); | ||||||
|  |  | ||||||
|  |     this.buildProjectForms = this.buildProjectForms.bind(this); | ||||||
|     this.handleSubmit = this.handleSubmit.bind(this); |     this.handleSubmit = this.handleSubmit.bind(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   componentDidMount() { |   componentDidMount() { | ||||||
|     const { projects } = this.props; |     const { modernProjects } = this.props; | ||||||
|     if (!projects.length) { |     if (!modernProjects.length) { | ||||||
|       this.props.fetchChallenges(); |       this.props.fetchChallenges(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   handleSubmit(values) { |   buildProjectForms({ | ||||||
|     const { id } = values; |     projectBlockName, | ||||||
|     const fullForm = _.values(values) |     challenges, | ||||||
|       .filter(Boolean) |     superBlock | ||||||
|       .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) { |  | ||||||
|       return this.props.claimCert(id); |  | ||||||
|     } |  | ||||||
|     const { projects } = this.props; |  | ||||||
|     const pIndex = _.findIndex(projects, p => p.superBlock === id); |  | ||||||
|     values.nameToIdMap = projects[pIndex].challengeNameIdMap; |  | ||||||
|     return this.props.updateUserBackend({ |  | ||||||
|       projects: { |  | ||||||
|         [id]: values |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   render() { |  | ||||||
|     const { |     const { | ||||||
|       blockNameIsCertMap, |       blockNameIsCertMap, | ||||||
|       claimCert, |       claimCert, | ||||||
|       hardGoTo, |       hardGoTo, | ||||||
|       projects, |  | ||||||
|       userProjects, |       userProjects, | ||||||
|       username |       username | ||||||
|     } = this.props; |     } = this.props; | ||||||
|     if (!projects.length) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|     return ( |  | ||||||
|       <div> |  | ||||||
|         <SectionHeader> |  | ||||||
|           Certification Settings |  | ||||||
|         </SectionHeader> |  | ||||||
|         <FullWidthRow> |  | ||||||
|         <p> |  | ||||||
|           Add links to the live demos of your projects as you finish them. |  | ||||||
|           Then, once you have added all 5 projects required for a certificate, |  | ||||||
|           you can claim it. |  | ||||||
|         </p> |  | ||||||
|         </FullWidthRow> |  | ||||||
|       { |  | ||||||
|         projects.map(({ |  | ||||||
|           projectBlockName, |  | ||||||
|           challenges, |  | ||||||
|           superBlock |  | ||||||
|         }) => { |  | ||||||
|     const isCertClaimed = blockNameIsCertMap[projectBlockName]; |     const isCertClaimed = blockNameIsCertMap[projectBlockName]; | ||||||
|  |     const challengeTitles = challenges | ||||||
|  |       .map(challenge => challenge.title || 'Unknown Challenge'); | ||||||
|     if (superBlock === jsProjectSuperBlock) { |     if (superBlock === jsProjectSuperBlock) { | ||||||
|       return ( |       return ( | ||||||
|         <JSAlgoAndDSForm |         <JSAlgoAndDSForm | ||||||
|                 challenges={ challenges } |           challenges={ challengeTitles } | ||||||
|           claimCert={ claimCert } |           claimCert={ claimCert } | ||||||
|           hardGoTo={ hardGoTo } |           hardGoTo={ hardGoTo } | ||||||
|           isCertClaimed={ isCertClaimed } |           isCertClaimed={ isCertClaimed } | ||||||
| @@ -172,7 +158,7 @@ class CertificationSettings extends PureComponent { | |||||||
|         /> |         /> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|           const options = challenges |     const options = challengeTitles | ||||||
|       .reduce((options, current) => { |       .reduce((options, current) => { | ||||||
|         options.types[current] = 'url'; |         options.types[current] = 'url'; | ||||||
|         return options; |         return options; | ||||||
| @@ -187,7 +173,7 @@ class CertificationSettings extends PureComponent { | |||||||
|       userValues.id = superBlock; |       userValues.id = superBlock; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|           const initialValues = challenges |     const initialValues = challengeTitles | ||||||
|       .reduce((accu, current) => ({ |       .reduce((accu, current) => ({ | ||||||
|         ...accu, |         ...accu, | ||||||
|         [current]: '' |         [current]: '' | ||||||
| @@ -199,14 +185,14 @@ class CertificationSettings extends PureComponent { | |||||||
|       // minus 1 to account for the id |       // minus 1 to account for the id | ||||||
|       .length - 1; |       .length - 1; | ||||||
|  |  | ||||||
|           const fullForm = completedProjects === challenges.length; |     const fullForm = completedProjects === challengeTitles.length; | ||||||
|     return ( |     return ( | ||||||
|       <FullWidthRow key={superBlock}> |       <FullWidthRow key={superBlock}> | ||||||
|         <h3 className='project-heading'>{ projectBlockName }</h3> |         <h3 className='project-heading'>{ projectBlockName }</h3> | ||||||
|         <Form |         <Form | ||||||
|           buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' } |           buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' } | ||||||
|           enableSubmit={ fullForm } |           enableSubmit={ fullForm } | ||||||
|                 formFields={ challenges.concat([ 'id' ]) } |           formFields={ challengeTitles.concat([ 'id' ]) } | ||||||
|           hideButton={isCertClaimed} |           hideButton={isCertClaimed} | ||||||
|           id={ superBlock } |           id={ superBlock } | ||||||
|           initialValues={{ |           initialValues={{ | ||||||
| @@ -222,7 +208,8 @@ class CertificationSettings extends PureComponent { | |||||||
|               block={ true } |               block={ true } | ||||||
|               bsSize='lg' |               bsSize='lg' | ||||||
|               bsStyle='primary' |               bsStyle='primary' | ||||||
|                     href={ `/c/${username}/${superBlock}`} |               href={ `/certificates/${username}/${superBlock}`} | ||||||
|  |               target='_blank' | ||||||
|               > |               > | ||||||
|               Show Certificate |               Show Certificate | ||||||
|             </Button> : |             </Button> : | ||||||
| @@ -231,7 +218,78 @@ class CertificationSettings extends PureComponent { | |||||||
|         <hr /> |         <hr /> | ||||||
|       </FullWidthRow> |       </FullWidthRow> | ||||||
|     ); |     ); | ||||||
|         }) |   } | ||||||
|  |  | ||||||
|  |   handleSubmit(values) { | ||||||
|  |     const { id } = values; | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |     // 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 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]: valuesToIds | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   render() { | ||||||
|  |     const { | ||||||
|  |       modernProjects, | ||||||
|  |       legacyProjects | ||||||
|  |     } = this.props; | ||||||
|  |     if (!modernProjects.length) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return ( | ||||||
|  |       <div> | ||||||
|  |         <SectionHeader> | ||||||
|  |           Certification Settings | ||||||
|  |         </SectionHeader> | ||||||
|  |         <FullWidthRow> | ||||||
|  |         <p> | ||||||
|  |           Add links to the live demos of your projects as you finish them. | ||||||
|  |           Then, once you have added all 5 projects required for a certificate, | ||||||
|  |           you can claim it. | ||||||
|  |         </p> | ||||||
|  |         </FullWidthRow> | ||||||
|  |         { | ||||||
|  |           modernProjects.map(this.buildProjectForms) | ||||||
|  |         } | ||||||
|  |         <SectionHeader> | ||||||
|  |           Legacy Certificate Settings | ||||||
|  |         </SectionHeader> | ||||||
|  |         { | ||||||
|  |           legacyProjects.map(this.buildProjectForms) | ||||||
|         } |         } | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ class JSAlgoAndDSForm extends PureComponent { | |||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     const { username, superBlock, isCertClaimed } = this.props; |     const { username, superBlock, isCertClaimed } = this.props; | ||||||
|     if (isCertClaimed) { |     if (isCertClaimed) { | ||||||
|       return this.props.hardGoTo(`/c/${username}/${superBlock}`); |       return this.props.hardGoTo(`/certificates/${username}/${superBlock}`); | ||||||
|     } |     } | ||||||
|     return this.props.claimCert(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 const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures'; | ||||||
|  |  | ||||||
| export function buildUserProjectsMap(projectBlock, challengeMap) { | export function buildUserProjectsMap(projectBlock, challengeMap) { | ||||||
|   const { |   const { | ||||||
|     challengeNameIdMap, |  | ||||||
|     challenges, |     challenges, | ||||||
|     superBlock |     superBlock | ||||||
|   } = projectBlock; |   } = projectBlock; | ||||||
|   return { |   return { | ||||||
|     [superBlock]: challenges.reduce((solutions, current) => { |     [superBlock]: challenges.reduce((solutions, current) => { | ||||||
|       const dashedName = dasherize(current) |       const { id } = current; | ||||||
|         .replace('java-script', 'javascript') |       const completed = challengeMap[id]; | ||||||
|         .replace('metric-imperial', 'metricimperial'); |  | ||||||
|       const completed = challengeMap[challengeNameIdMap[dashedName]]; |  | ||||||
|       let solution = ''; |       let solution = ''; | ||||||
|       if (superBlock === jsProjectSuperBlock) { |       if (superBlock === jsProjectSuperBlock) { | ||||||
|         solution = {}; |         solution = {}; | ||||||
| @@ -25,7 +20,7 @@ export function buildUserProjectsMap(projectBlock, challengeMap) { | |||||||
|       } |       } | ||||||
|       return { |       return { | ||||||
|         ...solutions, |         ...solutions, | ||||||
|         [current]: solution |         [current.title]: solution | ||||||
|       }; |       }; | ||||||
|     }, {}) |     }, {}) | ||||||
|   }; |   }; | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import loopback from 'loopback'; | |||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
|  |  | ||||||
| import { themes } from '../utils/themes'; | import { themes } from '../utils/themes'; | ||||||
| import { dasherize } from '../../server/utils'; |  | ||||||
| import { saveUser, observeMethod } from '../../server/utils/rx.js'; | import { saveUser, observeMethod } from '../../server/utils/rx.js'; | ||||||
| import { blacklistedUsernames } from '../../server/utils/constants.js'; | import { blacklistedUsernames } from '../../server/utils/constants.js'; | ||||||
| import { wrapHandledError } from '../../server/utils/create-handled-error.js'; | import { wrapHandledError } from '../../server/utils/create-handled-error.js'; | ||||||
| @@ -43,28 +42,24 @@ function destroyAll(id, Model) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function buildChallengeMapUpdate(challengeMap, project) { | function buildChallengeMapUpdate(challengeMap, project) { | ||||||
|  |   const key = Object.keys(project)[0]; | ||||||
|  |   const solutions = project[key]; | ||||||
|   const currentChallengeMap = { ...challengeMap }; |   const currentChallengeMap = { ...challengeMap }; | ||||||
|   const { nameToIdMap } = _.values(project)[0]; |   const currentCompletedProjects = _.pick( | ||||||
|   const incomingUpdate = _.pickBy( |     currentChallengeMap, | ||||||
|     _.omit(_.values(project)[0], [ 'id', 'nameToIdMap' ]), |     Object.keys(solutions) | ||||||
|     Boolean |  | ||||||
|   ); |   ); | ||||||
|   const currentCompletedProjects = _.pick(challengeMap, _.values(nameToIdMap)); |  | ||||||
|   const now = Date.now(); |   const now = Date.now(); | ||||||
|   const update = Object.keys(incomingUpdate).reduce((update, current) => { |   const update = Object.keys(solutions).reduce((update, currentId) => { | ||||||
|     const dashedName = dasherize(current) |  | ||||||
|       .replace('java-script', 'javascript') |  | ||||||
|       .replace('metric-imperial', 'metricimperial'); |  | ||||||
|     const currentId = nameToIdMap[dashedName]; |  | ||||||
|     if ( |     if ( | ||||||
|       currentId in currentCompletedProjects && |       currentId in currentCompletedProjects && | ||||||
|       currentCompletedProjects[currentId].solution !== incomingUpdate[current] |       currentCompletedProjects[currentId].solution !== solutions[currentId] | ||||||
|     ) { |     ) { | ||||||
|       return { |       return { | ||||||
|         ...update, |         ...update, | ||||||
|         [currentId]: { |         [currentId]: { | ||||||
|           ...currentCompletedProjects[currentId], |           ...currentCompletedProjects[currentId], | ||||||
|           solution: incomingUpdate[current], |           solution: solutions[currentId], | ||||||
|           numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1 |           numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1 | ||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
| @@ -74,7 +69,7 @@ function buildChallengeMapUpdate(challengeMap, project) { | |||||||
|         ...update, |         ...update, | ||||||
|         [currentId]: { |         [currentId]: { | ||||||
|           id: currentId, |           id: currentId, | ||||||
|           solution: incomingUpdate[current], |           solution: solutions[currentId], | ||||||
|           challengeType: 3, |           challengeType: 3, | ||||||
|           completedDate: now, |           completedDate: now, | ||||||
|           numOfAttempts: 1 |           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": {} |       "translations": {} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "id": "587d78b0367417b2b2512b06", |       "id": "bd7158d8c242eddfaeb5bd13", | ||||||
|       "title": "Build a Personal Portfolio Webpage", |       "title": "Build a Personal Portfolio Webpage", | ||||||
|       "description": [ |       "description": [ | ||||||
|         "Fulfill the user stories by getting all of the tests to pass. Use whichever libraries you need. Give it your own personal style.", |         "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", |       "id": "bd7158d8c442eddfaeb5bd1f", | ||||||
|       "title": "Use the Twitch.tv JSON API", |       "title": "Use the Twitch JSON API", | ||||||
|       "description": [ |       "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>.", |         "<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.", |         "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, |       "isRequired": false, | ||||||
|       "translations": { |       "translations": { | ||||||
|         "es": { |         "es": { | ||||||
|           "title": "Usa el API JSON de Twitch.tv", |           "title": "Usa el API JSON de Twitch", | ||||||
|           "description": [ |           "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>.", |             "<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.", |             "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": { |         "ru": { | ||||||
|           "title": "Используйте Twitch.tv JSON API", |           "title": "Используйте Twitch JSON API", | ||||||
|           "description": [ |           "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'>Задание:</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 примера. Напишите его самостоятельно.", |             "<span class='text-info'>Правило #1:</span> Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.", | ||||||
| @@ -682,6 +682,127 @@ | |||||||
|       "isRequired": false, |       "isRequired": false, | ||||||
|       "titleEs": "Crea un clon de Pinterest" |       "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", |       "id": "5a4b7fcdb66f799f199e11db", | ||||||
|       "title": "Build a Pong Game", |       "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 _ from 'lodash'; | ||||||
| import loopback from 'loopback'; | import loopback from 'loopback'; | ||||||
|  | import moment from 'moment-timezone'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
| import dedent from 'dedent'; | import dedent from 'dedent'; | ||||||
| import { Observable } from 'rx'; | import { Observable } from 'rx'; | ||||||
| @@ -13,26 +14,23 @@ import { | |||||||
| import { observeQuery } from '../utils/rx'; | import { observeQuery } from '../utils/rx'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   // legacy |   legacyFrontEndChallengeId, | ||||||
|   frontEndChallengeId, |   legacyBackEndChallengeId, | ||||||
|   backEndChallengeId, |   legacyDataVisId, | ||||||
|   dataVisId, |  | ||||||
|  |  | ||||||
|   // modern |  | ||||||
|   respWebDesignId, |   respWebDesignId, | ||||||
|   frontEndLibsId, |   frontEndLibsId, | ||||||
|   dataVis2018Id, |  | ||||||
|   jsAlgoDataStructId, |   jsAlgoDataStructId, | ||||||
|  |   dataVis2018Id, | ||||||
|   apisMicroservicesId, |   apisMicroservicesId, | ||||||
|   infosecQaId |   infosecQaId | ||||||
| } from '../utils/constantStrings.json'; | } from '../utils/constantStrings.json'; | ||||||
|  | import certTypes from '../utils/certTypes.json'; | ||||||
|  | import superBlockCertTypeMap from '../utils/superBlockCertTypeMap'; | ||||||
| import { | import { | ||||||
|   completeCommitment$ |   completeCommitment$ | ||||||
| } from '../utils/commit'; | } from '../utils/commit'; | ||||||
|  |  | ||||||
| import certTypes from '../utils/certTypes.json'; |  | ||||||
| import superBlockCertTypeMap from '../utils/superBlockCertTypeMap'; |  | ||||||
|  |  | ||||||
| const log = debug('fcc:certification'); | const log = debug('fcc:certification'); | ||||||
| const renderCertifedEmail = loopback.template(path.join( | const renderCertifedEmail = loopback.template(path.join( | ||||||
|   __dirname, |   __dirname, | ||||||
| @@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) { | |||||||
|   return _.every(ids, ({ id }) => _.has(challengeMap, id)); |   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) { | function getIdsForCert$(id, Challenge) { | ||||||
|   return observeQuery( |   return observeQuery( | ||||||
|     Challenge, |     Challenge, | ||||||
| @@ -117,13 +157,24 @@ function sendCertifiedEmail( | |||||||
|  |  | ||||||
| export default function certificate(app) { | export default function certificate(app) { | ||||||
|   const router = app.loopback.Router(); |   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 = { |   const certTypeIds = { | ||||||
|     // legacy |     // legacy | ||||||
|     [certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge), |     [certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge), | ||||||
|     [certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge), |     [certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge), | ||||||
|     [certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge), |     [certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge), | ||||||
|  |  | ||||||
|     // modern |     // modern | ||||||
|     [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), |     [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), | ||||||
| @@ -145,6 +196,10 @@ export default function certificate(app) { | |||||||
|     ifNoSuperBlock404, |     ifNoSuperBlock404, | ||||||
|     verifyCert |     verifyCert | ||||||
|   ); |   ); | ||||||
|  |   router.get( | ||||||
|  |     '/certificates/:username/:cert', | ||||||
|  |     showCert | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   app.use(router); |   app.use(router); | ||||||
|  |  | ||||||
| @@ -172,13 +227,9 @@ export default function certificate(app) { | |||||||
|  |  | ||||||
|   function verifyCert(req, res, next) { |   function verifyCert(req, res, next) { | ||||||
|     const { body: { superBlock }, user } = req; |     const { body: { superBlock }, user } = req; | ||||||
|  |     log(superBlock); | ||||||
|     let certType = superBlockCertTypeMap[superBlock]; |     let certType = superBlockCertTypeMap[superBlock]; | ||||||
|     log(certType); |     log(certType); | ||||||
|     if (certType === 'isDataVisCert') { |  | ||||||
|       certType = 'is2018DataVisCert'; |  | ||||||
|       log(certType); |  | ||||||
|     } |  | ||||||
|     return user.getChallengeMap$() |     return user.getChallengeMap$() | ||||||
|       .flatMap(() => certTypeIds[certType]) |       .flatMap(() => certTypeIds[certType]) | ||||||
|       .flatMap(challenge => { |       .flatMap(challenge => { | ||||||
| @@ -251,4 +302,101 @@ export default function certificate(app) { | |||||||
|     } |     } | ||||||
|     return res.status(404).end(); |     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 dedent from 'dedent'; | ||||||
| import moment from 'moment-timezone'; |  | ||||||
| import debugFactory from 'debug'; | import debugFactory from 'debug'; | ||||||
| import { curry } from 'lodash'; | 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 { | import { | ||||||
|   ifNoUser401, |   ifNoUser401, | ||||||
|   ifNoUserRedirectTo, |   ifNoUserRedirectTo, | ||||||
|   ifNotVerifiedRedirectToSettings |   ifNotVerifiedRedirectToSettings | ||||||
| } from '../utils/middleware'; | } from '../utils/middleware'; | ||||||
| import { observeQuery } from '../utils/rx'; |  | ||||||
|  |  | ||||||
| const debug = debugFactory('fcc:boot:user'); | const debug = debugFactory('fcc:boot:user'); | ||||||
| const sendNonUserToMap = ifNoUserRedirectTo('/map'); | const sendNonUserToMap = ifNoUserRedirectTo('/map'); | ||||||
| const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/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) { | module.exports = function(app) { | ||||||
|   const router = app.loopback.Router(); |   const router = app.loopback.Router(); | ||||||
|   const api = app.loopback.Router(); |   const api = app.loopback.Router(); | ||||||
|   const { Email, User } = app.models; |   const { Email, User } = app.models; | ||||||
|  |  | ||||||
|   function findUserByUsername$(username, fields) { |  | ||||||
|     return observeQuery( |  | ||||||
|       User, |  | ||||||
|       'findOne', |  | ||||||
|       { |  | ||||||
|         where: { username }, |  | ||||||
|         fields |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   api.post( |   api.post( | ||||||
|     '/account/delete', |     '/account/delete', | ||||||
|     ifNoUser401, |     ifNoUser401, | ||||||
| @@ -105,11 +39,6 @@ module.exports = function(app) { | |||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   // Ensure these are the last routes! |   // Ensure these are the last routes! | ||||||
|   api.get( |  | ||||||
|     '/c/:username/:cert', |  | ||||||
|     showCert |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   router.get( |   router.get( | ||||||
|     '/user/:username/report-user/', |     '/user/:username/report-user/', | ||||||
|     sendNonUserToMapWithMessage('You must be signed in to report a 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) { |   function postDeleteAccount(req, res, next) { | ||||||
|     User.destroyById(req.user.id, function(err) { |     User.destroyById(req.user.id, function(err) { | ||||||
|       if (err) { return next(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", |   "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", |   "legacyFrontEndChallengeId": "561add10cb82ac38a17513be", | ||||||
|   "backEndChallengeId": "660add10cb82ac38a17513be", |   "legacyBackEndChallengeId": "660add10cb82ac38a17513be", | ||||||
|   "dataVisId": "561add10cb82ac39a17513bc", |   "legacyDataVisId": "561add10cb82ac39a17513bc", | ||||||
|  |  | ||||||
|   "respWebDesignId": "561add10cb82ac38a17513bc", |   "respWebDesignId": "561add10cb82ac38a17513bc", | ||||||
|   "frontEndLibsId": "561acd10cb82ac38a17513bc", |   "frontEndLibsId": "561acd10cb82ac38a17513bc", | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ export const publicUserProps = [ | |||||||
|   'isApisMicroservicesCert', |   'isApisMicroservicesCert', | ||||||
|   'isBackEndCert', |   'isBackEndCert', | ||||||
|   'isCheater', |   'isCheater', | ||||||
|  |   'is2018DataVisCert', | ||||||
|   'isDataVisCert', |   'isDataVisCert', | ||||||
|   'isFrontEndCert', |   'isFrontEndCert', | ||||||
|   'isFullStackCert', |   'isFullStackCert', | ||||||
|   | |||||||
| @@ -2,16 +2,16 @@ import certTypes from './certTypes.json'; | |||||||
|  |  | ||||||
| const superBlockCertTypeMap = { | const superBlockCertTypeMap = { | ||||||
|   // legacy |   // legacy | ||||||
|   'front-end': certTypes.frontEnd, |   'legacy-front-end': certTypes.frontEnd, | ||||||
|   'back-end': certTypes.backEnd, |   'legacy-back-end': certTypes.backEnd, | ||||||
|   'data-visualization': certTypes.dataVis, |   'legacy-data-visualization': certTypes.dataVis, | ||||||
|   'full-stack': certTypes.fullStack, |   'legacy-full-stack': certTypes.fullStack, | ||||||
|  |  | ||||||
|   // modern |   // modern | ||||||
|   'responsive-web-design': certTypes.respWebDesign, |   'responsive-web-design': certTypes.respWebDesign, | ||||||
|   'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct, |   'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct, | ||||||
|   'front-end-libraries': certTypes.frontEndLibs, |   'front-end-libraries': certTypes.frontEndLibs, | ||||||
|   'data-visualization-2018': certTypes.dataVis2018, |   'data-visualization': certTypes.dataVis2018, | ||||||
|   'apis-and-microservices': certTypes.apisMicroservices, |   'apis-and-microservices': certTypes.apisMicroservices, | ||||||
|   'information-security-and-quality-assurance': certTypes.infosecQa |   'information-security-and-quality-assurance': certTypes.infosecQa | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -29,4 +29,4 @@ include styles | |||||||
| 					strong Quincy Larson | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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') | 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') | 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 | .certificate-wrapper.container | ||||||
| 	.row | 	.row | ||||||
| @@ -29,4 +29,4 @@ include styles | |||||||
| 					strong Quincy Larson | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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') | 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') | 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 | .certificate-wrapper.container | ||||||
| 	.row | 	.row | ||||||
| @@ -29,4 +29,4 @@ include styles | |||||||
| 					strong Quincy Larson | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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') | 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') | 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 | .certificate-wrapper.container | ||||||
| 	.row | 	.row | ||||||
| @@ -19,8 +19,8 @@ include styles | |||||||
| 					strong= name | 					strong= name | ||||||
| 				h3 has successfully completed freeCodeCamp's | 				h3 has successfully completed freeCodeCamp's | ||||||
| 				h1 | 				h1 | ||||||
| 					strong Full Stack Development Projects | 					strong Legacy Full Stack Development Program | ||||||
| 				h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework | 				h4 All three of the legacy freeCodeCamp certificates, representing approximately 12000 hours of coursework | ||||||
| 
 | 
 | ||||||
| 		footer | 		footer | ||||||
| 			.row.signatures | 			.row.signatures | ||||||
| @@ -29,4 +29,4 @@ include styles | |||||||
| 					strong Quincy Larson | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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 | 					strong Quincy Larson | ||||||
| 				p Executive Director, freeCodeCamp.org | 				p Executive Director, freeCodeCamp.org | ||||||
| 			.row | 			.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