diff --git a/client/package-lock.json b/client/package-lock.json index ebdc05a379..96aa3f9848 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2966,6 +2966,17 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, + "clipboard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.1.tgz", + "integrity": "sha512-7yhQBmtN+uYZmfRjjVjKa0dZdWuabzpSKGtyQZN+9C8xlC788SSJjOHWh7tzurfwTqTD5UDYAhIv5fRJg3sHjQ==", + "optional": true, + "requires": { + "good-listener": "1.2.2", + "select": "1.1.2", + "tiny-emitter": "2.0.2" + } + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -3780,6 +3791,12 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -6670,6 +6687,15 @@ } } }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "optional": true, + "requires": { + "delegate": "3.2.0" + } + }, "got": { "version": "6.7.1", "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", @@ -10263,6 +10289,14 @@ } } }, + "prismjs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz", + "integrity": "sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA==", + "requires": { + "clipboard": "2.0.1" + } + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -11455,6 +11489,12 @@ "invariant": "2.2.4" } }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "optional": true + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -12473,6 +12513,12 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-emitter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", + "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==", + "optional": true + }, "tmp": { "version": "0.0.31", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", diff --git a/client/package.json b/client/package.json index bdb7979190..2c78def320 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "gatsby-plugin-sitemap": "^2.0.0-rc.1", "lodash": "^4.17.10", "nanoid": "^1.2.2", + "prismjs": "^1.15.0", "query-string": "^6.1.0", "react": "^16.4.2", "react-dom": "^16.4.2", diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index 3ee698f80e..de8f97daf3 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -19,82 +19,70 @@ import Email from '../components/settings/Email'; import Internet from '../components/settings/Internet'; import Portfolio from '../components/settings/Portfolio'; import Honesty from '../components/settings/Honesty'; +import Certification from '../components/settings/Certification'; const propTypes = { - about: PropTypes.string, - email: PropTypes.string, - githubProfile: PropTypes.string, - isEmailVerified: PropTypes.bool, - isHonest: PropTypes.bool, - linkedin: PropTypes.string, - location: PropTypes.string, - name: PropTypes.string, - picture: PropTypes.string, - points: PropTypes.number, - portfolio: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - title: PropTypes.string, - url: PropTypes.string, - image: PropTypes.string, - description: PropTypes.string - }) - ), - sendQuincyEmail: PropTypes.bool, showLoading: PropTypes.bool, submitNewAbout: PropTypes.func.isRequired, - theme: PropTypes.string, toggleNightMode: PropTypes.func.isRequired, - twitter: PropTypes.string, updateInternetSettings: PropTypes.func.isRequired, updateIsHonest: PropTypes.func.isRequired, updatePortfolio: PropTypes.func.isRequired, updateQuincyEmail: PropTypes.func.isRequired, - username: PropTypes.string, - website: PropTypes.string + user: PropTypes.shape({ + about: PropTypes.string, + completedChallenges: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + solution: PropTypes.string, + githubLink: PropTypes.string, + challengeType: PropTypes.number, + completedDate: PropTypes.number, + files: PropTypes.array + }) + ), + email: PropTypes.string, + githubProfile: PropTypes.string, + is2018DataVisCert: PropTypes.bool, + isApisMicroservicesCert: PropTypes.bool, + isBackEndCert: PropTypes.bool, + isDataVisCert: PropTypes.bool, + isEmailVerified: PropTypes.bool, + isFrontEndCert: PropTypes.bool, + isFrontEndLibsCert: PropTypes.bool, + isFullStackCert: PropTypes.bool, + isHonest: PropTypes.bool, + isInfosecQaCert: PropTypes.bool, + isJsAlgoDataStructCert: PropTypes.bool, + isRespWebDesignCert: PropTypes.bool, + linkedin: PropTypes.string, + location: PropTypes.string, + name: PropTypes.string, + picture: PropTypes.string, + points: PropTypes.number, + portfolio: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string, + url: PropTypes.string, + image: PropTypes.string, + description: PropTypes.string + }) + ), + sendQuincyEmail: PropTypes.bool, + theme: PropTypes.string, + twitter: PropTypes.string, + username: PropTypes.string, + website: PropTypes.string + }) }; const mapStateToProps = createSelector( signInLoadingSelector, userSelector, - ( + (showLoading, user) => ({ showLoading, - { - username = '', - about, - email, - sendQuincyEmail, - isEmailVerified, - isHonest, - picture, - points, - name, - location, - theme, - githubProfile, - linkedin, - twitter, - website, - portfolio - } - ) => ({ - email, - sendQuincyEmail, - isEmailVerified, - isHonest, - showLoading, - username, - about, - picture, - points, - name, - theme, - location, - githubProfile, - linkedin, - twitter, - website, - portfolio + user }) ); @@ -113,27 +101,39 @@ const mapDispatchToProps = dispatch => function ShowSettings(props) { const { - email, - isEmailVerified, - isHonest, - sendQuincyEmail, - showLoading, - username, - about, - picture, - points, - theme, - location, - name, submitNewAbout, toggleNightMode, + user: { + completedChallenges, + email, + is2018DataVisCert, + isApisMicroservicesCert, + isJsAlgoDataStructCert, + isBackEndCert, + isDataVisCert, + isFrontEndCert, + isInfosecQaCert, + isFrontEndLibsCert, + isFullStackCert, + isEmailVerified, + isHonest, + sendQuincyEmail, + username, + about, + picture, + points, + theme, + location, + name, + githubProfile, + linkedin, + twitter, + website, + portfolio + }, + showLoading, updateQuincyEmail, - githubProfile, - linkedin, - twitter, - website, updateInternetSettings, - portfolio, updatePortfolio, updateIsHonest } = props; @@ -208,11 +208,22 @@ function ShowSettings(props) { - + - {/* + - */} + {/* */} ); diff --git a/client/src/components/settings/Certification.js b/client/src/components/settings/Certification.js new file mode 100644 index 0000000000..6b5df019fb --- /dev/null +++ b/client/src/components/settings/Certification.js @@ -0,0 +1,196 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { find } from 'lodash'; +import { + Table, + Button, + DropdownButton, + MenuItem, + Modal +} from '@freecodecamp/react-bootstrap'; +import { Link, navigate } from 'gatsby'; + +import { projectMap } from '../../resources/certProjectMap'; + +import SectionHeader from './SectionHeader'; +import SolutionViewer from './SolutionViewer'; +import { FullWidthRow } from '../helpers'; +import { maybeUrlRE } from '../../utils'; + +const propTypes = { + completedChallenges: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + solution: PropTypes.string, + githubLink: PropTypes.string, + challengeType: PropTypes.number, + completedDate: PropTypes.number, + files: PropTypes.array + }) + ) +}; + +const certifications = Object.keys(projectMap); +const initialState = { + solutionViewer: { + projectTitle: '', + files: null, + solution: null, + isOpen: false + } +}; + +class CertificationSettings extends Component { + constructor(props) { + super(props); + + this.state = { ...initialState }; + } + + createHandleLinkButtonClick = to => e => { + e.preventDefault(); + return navigate(to); + }; + handleSolutionModalHide = () => this.setState({ ...initialState }); + + getProjectSolution = (projectId, projectTitle) => { + const { completedChallenges } = this.props; + const completedProject = find( + completedChallenges, + ({ id }) => projectId === id + ); + if (!completedProject) { + return null; + } + const { solution, githubLink, files } = completedProject; + const onClickHandler = () => + this.setState({ + solutionViewer: { + projectTitle, + files, + solution, + isOpen: true + } + }); + if (files && files.length) { + return ( + + Show Code + + ); + } + if (githubLink) { + return ( + + + Front End + + + Back End + + + ); + } + if (maybeUrlRE.test(solution)) { + return ( + + Show Solution + + ); + } + return ( + + Show Code + + ); + }; + + renderCertifications = certName => ( + + {certName} + + + + Project Name + Solution + + + {this.renderProjectsFor(certName)} + + + ); + + renderProjectsFor = certName => + projectMap[certName].map(({ link, title, id }) => ( + + + {title} + + + {this.getProjectSolution(id, title)} + + + )); + + render() { + const { + solutionViewer: { files, solution, isOpen, projectTitle } + } = this.state; + return ( + + Certifications + {certifications.map(this.renderCertifications)} + {isOpen ? ( + + + + Solution for {projectTitle} + + + + + + + Close + + + ) : null} + + ); + } +} + +CertificationSettings.displayName = 'CertificationSettings'; +CertificationSettings.propTypes = propTypes; + +export default CertificationSettings; diff --git a/client/src/components/settings/SolutionViewer.js b/client/src/components/settings/SolutionViewer.js new file mode 100644 index 0000000000..a63f7806b7 --- /dev/null +++ b/client/src/components/settings/SolutionViewer.js @@ -0,0 +1,79 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Panel } from '@freecodecamp/react-bootstrap'; +import Prism from 'prismjs'; +import Helmet from 'react-helmet'; + +import './solution-viewer.css'; + +const prismLang = { + css: 'css', + js: 'javascript', + jsx: 'javascript', + html: 'markup' +}; + +function SolutionViewer({ + files, + solution = '// The solution is not available for this project' +}) { + const solutions = + files && Array.isArray(files) && files.length ? ( + files.map(file => ( + + {file.ext.toUpperCase()} + + + + + + + )) + ) : ( + + JS + + + + + + + ); + return ( + + + + + {solutions} + + ); +} + +SolutionViewer.displayName = 'SolutionViewer'; +SolutionViewer.propTypes = { + files: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)), + solution: PropTypes.string +}; + +export default SolutionViewer; diff --git a/client/src/components/settings/certification.css b/client/src/components/settings/certification.css new file mode 100644 index 0000000000..fa9d50b5d3 --- /dev/null +++ b/client/src/components/settings/certification.css @@ -0,0 +1,13 @@ +#certifcation-settings .project-title { + display: flex; +} + +#certifcation-settings .project-solution { + display: flex; + +} + +#certifcation-settings .project-row { + display: flex; + +} \ No newline at end of file diff --git a/client/src/components/settings/solution-viewer.css b/client/src/components/settings/solution-viewer.css new file mode 100644 index 0000000000..a72ea94146 --- /dev/null +++ b/client/src/components/settings/solution-viewer.css @@ -0,0 +1,3 @@ +.solution-viewer, .solution-viewer pre { + margin-bottom: 0px; +} \ No newline at end of file diff --git a/client/src/resources/certProjectMap.js b/client/src/resources/certProjectMap.js new file mode 100644 index 0000000000..5cbacf03ca --- /dev/null +++ b/client/src/resources/certProjectMap.js @@ -0,0 +1,177 @@ +const responsiveWebBase = + '/learn/responsive-web-design/responsive-web-design-projects'; +const jsAlgoBase = + '/learn/javascript-algorithms-and-data-structures/' + + 'javascript-algorithms-and-data-structures-projects'; +const feLibsBase = '/learn/front-end-libraries/front-end-libraries-projects'; +const dataVisBase = '/learn/data-visualization/data-visualization-projects'; +const apiMicroBase = + '/learn/apis-and-microservices/apis-and-microservices-projects'; +const infoSecBase = + '/learn/information-security-and-quality-assurance/' + + 'information-security-and-quality-assurance-projects'; + +export const projectMap = { + 'Responsive Web Design': [ + { + id: 'bd7158d8c442eddfaeb5bd18', + title: 'Build a Tribute Page', + link: `${responsiveWebBase}/build-a-tribute-page` + }, + { + id: '587d78af367417b2b2512b03', + title: 'Build a Survey Form', + link: `${responsiveWebBase}/build-a-survey-form` + }, + { + id: '587d78af367417b2b2512b04', + title: 'Build a Product Landing Page', + link: `${responsiveWebBase}/build-a-product-landing-page` + }, + { + id: '587d78b0367417b2b2512b05', + title: 'Build a Technical Documentation Page', + link: `${responsiveWebBase}/build-a-technical-documentation-page` + }, + { + id: 'bd7158d8c242eddfaeb5bd13', + title: 'Build a Personal Portfolio Webpage', + link: `${responsiveWebBase}/build-a-personal-portfolio-webpage` + } + ], + 'JavaScript Algorithms and Data Structures': [ + { + id: 'aaa48de84e1ecc7c742e1124', + title: 'Palindrome Checker', + link: `${jsAlgoBase}/palindrome-checker` + }, + { + id: 'a7f4d8f2483413a6ce226cac', + title: 'Roman Numeral Converter', + link: `${jsAlgoBase}/roman-numeral-converter` + }, + { + id: '56533eb9ac21ba0edf2244e2', + title: 'Caesars Cipher', + link: `${jsAlgoBase}/caesars-cipher` + }, + { + id: 'aff0395860f5d3034dc0bfc9', + title: 'Telephone Number Validator', + link: `${jsAlgoBase}/telephone-number-validator` + }, + { + id: 'aa2e6f85cab2ab736c9a9b24', + title: 'Cash Register', + link: `${jsAlgoBase}/cash-register` + } + ], + 'Front End Libraries': [ + { + id: 'bd7158d8c442eddfaeb5bd13', + title: 'Build a Random Quote Machine', + link: `${feLibsBase}/build-a-random-quote-machine` + }, + { + id: 'bd7157d8c242eddfaeb5bd13', + title: 'Build a Markdown Previewer', + link: `${feLibsBase}/build-a-markdown-previewer` + }, + { + id: '587d7dbc367417b2b2512bae', + title: 'Build a Drum Machine', + link: `${feLibsBase}/build-a-drum-machine` + }, + { + id: 'bd7158d8c442eddfaeb5bd17', + title: 'Build a JavaScript Calculator', + link: `${feLibsBase}/build-a-javascript-calculator` + }, + { + id: 'bd7158d8c442eddfaeb5bd0f', + title: 'Build a Pomodoro Clock', + link: `${feLibsBase}/build-a-pomodoro-clock` + } + ], + 'Data Visualization': [ + { + id: 'bd7168d8c242eddfaeb5bd13', + title: 'Visualize Data with a Bar Chart', + link: `${dataVisBase}/visualize-data-with-a-bar-chart` + }, + { + id: 'bd7178d8c242eddfaeb5bd13', + title: 'Visualize Data with a Scatterplot Graph', + link: `${dataVisBase}/visualize-data-with-a-scatterplot-graph` + }, + { + id: 'bd7188d8c242eddfaeb5bd13', + title: 'Visualize Data with a Heat Map', + link: `${dataVisBase}/visualize-data-with-a-heat-map` + }, + { + id: '587d7fa6367417b2b2512bbf', + title: 'Visualize Data with a Choropleth Map', + link: `${dataVisBase}/visualize-data-with-a-choropleth-map` + }, + { + id: '587d7fa6367417b2b2512bc0', + title: 'Visualize Data with a Treemap Diagram', + link: `${dataVisBase}/visualize-data-with-a-treemap-diagram` + } + ], + "API's and Microservices": [ + { + id: 'bd7158d8c443edefaeb5bdef', + title: 'Timestamp Microservice', + link: `${apiMicroBase}/timestamp-microservice` + }, + { + id: 'bd7158d8c443edefaeb5bdff', + title: 'Request Header Parser Microservice', + link: `${apiMicroBase}/request-header-parser-microservice` + }, + { + id: 'bd7158d8c443edefaeb5bd0e', + title: 'URL Shortener Microservice', + link: `${apiMicroBase}/url-shortener-microservice` + }, + { + id: '5a8b073d06fa14fcfde687aa', + title: 'Exercise Tracker', + link: `${apiMicroBase}/exercise-tracker` + }, + { + id: 'bd7158d8c443edefaeb5bd0f', + title: 'File Metadata Microservice', + link: `${apiMicroBase}/file-metadata-microservice` + } + ], + 'Information Security And Quality Assurance': [ + { + id: '587d8249367417b2b2512c41', + title: 'Metric-Imperial Converter', + link: `${infoSecBase}/metric-imperial-converter` + }, + { + id: '587d8249367417b2b2512c42', + title: 'Issue Tracker', + link: `${infoSecBase}/issue-tracker` + }, + { + id: '587d824a367417b2b2512c43', + title: 'Personal Library', + link: `${infoSecBase}/personal-library` + }, + { + id: '587d824a367417b2b2512c44', + title: 'Stock Price Checker', + link: `${infoSecBase}/stock-price-checker` + }, + { + id: '587d824a367417b2b2512c45', + title: 'Anonymous Message Board', + link: `${infoSecBase}/anonymous-message-board` + } + ] +}; diff --git a/client/static/css/prism.css b/client/static/css/prism.css new file mode 100644 index 0000000000..b75ef446ce --- /dev/null +++ b/client/static/css/prism.css @@ -0,0 +1,138 @@ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + + code[class*="language-"], + pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + } + + pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, + code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; + } + + pre[class*="language-"]::selection, pre[class*="language-"] ::selection, + code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; + } + + @media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } + } + + /* Code blocks */ + pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; + } + + :not(pre) > code[class*="language-"], + pre[class*="language-"] { + background: #f5f2f0; + } + + /* Inline code */ + :not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; + } + + .token.comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: slategray; + } + + .token.punctuation { + color: #999; + } + + .namespace { + opacity: .7; + } + + .token.property, + .token.tag, + .token.boolean, + .token.number, + .token.constant, + .token.symbol, + .token.deleted { + color: #905; + } + + .token.selector, + .token.attr-name, + .token.string, + .token.char, + .token.builtin, + .token.inserted { + color: #690; + } + + .token.operator, + .token.entity, + .token.url, + .language-css .token.string, + .style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); + } + + .token.atrule, + .token.attr-value, + .token.keyword { + color: #07a; + } + + .token.function, + .token.class-name { + color: #DD4A68; + } + + .token.regex, + .token.important, + .token.variable { + color: #e90; + } + + .token.important, + .token.bold { + font-weight: bold; + } + .token.italic { + font-style: italic; + } + + .token.entity { + cursor: help; + }
+ +