diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js
index b76147b162..505c713319 100644
--- a/client/src/client-only-routes/ShowCertification.js
+++ b/client/src/client-only-routes/ShowCertification.js
@@ -6,6 +6,8 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import format from 'date-fns/format';
import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap';
+
+import ShowProjectLinks from './ShowProjectLinks';
import FreeCodeCampLogo from '../assets/icons/FreeCodeCampLogo';
// eslint-disable-next-line max-len
import DonateForm from '../components/Donation/DonateForm';
@@ -18,7 +20,9 @@ import {
userFetchStateSelector,
usernameSelector,
isDonatingSelector,
- executeGA
+ executeGA,
+ userByNameSelector,
+ fetchProfileForUser
} from '../redux';
import { certMap } from '../../src/resources/certAndProjectMap';
import { createFlashMessage } from '../components/Flash/redux';
@@ -27,6 +31,7 @@ import reallyWeirdErrorMessage from '../utils/reallyWeirdErrorMessage';
import RedirectHome from '../components/RedirectHome';
import { Loader, Spacer } from '../components/helpers';
+import { isEmpty } from 'lodash';
const propTypes = {
cert: PropTypes.shape({
@@ -41,6 +46,7 @@ const propTypes = {
certName: PropTypes.string,
createFlashMessage: PropTypes.func.isRequired,
executeGA: PropTypes.func,
+ fetchProfileForUser: PropTypes.func,
fetchState: PropTypes.shape({
pending: PropTypes.bool,
complete: PropTypes.bool,
@@ -52,6 +58,25 @@ const propTypes = {
}),
showCert: PropTypes.func.isRequired,
signedInUserName: PropTypes.string,
+ user: PropTypes.shape({
+ completedChallenges: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ solution: PropTypes.string,
+ githubLink: PropTypes.string,
+ files: PropTypes.arrayOf(
+ PropTypes.shape({
+ contents: PropTypes.string,
+ ext: PropTypes.string,
+ key: PropTypes.string,
+ name: PropTypes.string,
+ path: PropTypes.string
+ })
+ )
+ })
+ ),
+ username: PropTypes.string
+ }),
userFetchState: PropTypes.shape({
complete: PropTypes.bool
}),
@@ -60,29 +85,37 @@ const propTypes = {
validCertName: PropTypes.bool
};
+const requestedUserSelector = (state, { username = '' }) =>
+ userByNameSelector(username.toLowerCase())(state);
+
const validCertNames = certMap.map(cert => cert.slug);
-const mapStateToProps = (state, { certName }) => {
- const validCertName = validCertNames.some(name => name === certName);
+const mapStateToProps = (state, props) => {
+ const validCertName = validCertNames.some(name => name === props.certName);
return createSelector(
showCertSelector,
showCertFetchStateSelector,
usernameSelector,
userFetchStateSelector,
isDonatingSelector,
- (cert, fetchState, signedInUserName, userFetchState, isDonating) => ({
+ requestedUserSelector,
+ (cert, fetchState, signedInUserName, userFetchState, isDonating, user) => ({
cert,
fetchState,
validCertName,
signedInUserName,
userFetchState,
- isDonating
+ isDonating,
+ user
})
);
};
const mapDispatchToProps = dispatch =>
- bindActionCreators({ createFlashMessage, showCert, executeGA }, dispatch);
+ bindActionCreators(
+ { createFlashMessage, showCert, fetchProfileForUser, executeGA },
+ dispatch
+ );
const ShowCertification = props => {
const { t } = useTranslation();
@@ -104,9 +137,17 @@ const ShowCertification = props => {
signedInUserName,
isDonating,
cert: { username = '' },
+ fetchProfileForUser,
+ user,
executeGA
} = props;
+ if (!signedInUserName || signedInUserName !== username) {
+ if (isEmpty(user) && username) {
+ fetchProfileForUser(username);
+ }
+ }
+
if (
!isDonationDisplayed &&
userComplete &&
@@ -125,7 +166,15 @@ const ShowCertification = props => {
}
});
}
- }, [isDonationDisplayed, props]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ isDonationDisplayed,
+ props.userFetchState,
+ props.signedInUserName,
+ props.isDonating,
+ props.cert,
+ props.executeGA
+ ]);
const hideDonationSection = () => {
setIsDonationDisplayed(false);
@@ -186,6 +235,7 @@ const ShowCertification = props => {
certTitle,
completionTime
} = cert;
+ const { user } = props;
const certDate = new Date(date);
const certYear = certDate.getFullYear();
@@ -311,6 +361,8 @@ const ShowCertification = props => {
{signedInUserName === username ? shareCertBtns : ''}
+
+
);
};
diff --git a/client/src/client-only-routes/ShowProjectLinks.js b/client/src/client-only-routes/ShowProjectLinks.js
new file mode 100644
index 0000000000..d3b6f037fd
--- /dev/null
+++ b/client/src/client-only-routes/ShowProjectLinks.js
@@ -0,0 +1,200 @@
+/* eslint-disable react/jsx-sort-props */
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import '../components/layouts/project-links.css';
+import { maybeUrlRE } from '../utils';
+import { Spacer, Link } from '../components/helpers';
+import { projectMap, legacyProjectMap } from '../resources/certAndProjectMap';
+import ProjectModal from '../components/SolutionViewer/ProjectModal';
+import { find, first } from 'lodash';
+
+const propTypes = {
+ certName: PropTypes.string,
+ name: PropTypes.string,
+ user: PropTypes.shape({
+ completedChallenges: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ solution: PropTypes.string,
+ githubLink: PropTypes.string,
+ files: PropTypes.arrayOf(
+ PropTypes.shape({
+ contents: PropTypes.string,
+ ext: PropTypes.string,
+ key: PropTypes.string,
+ name: PropTypes.string,
+ path: PropTypes.string
+ })
+ )
+ })
+ ),
+ username: PropTypes.string
+ })
+};
+
+const initSolutionState = {
+ projectTitle: '',
+ files: null,
+ solution: null,
+ isOpen: false
+};
+
+const ShowProjectLinks = props => {
+ const [solutionState, setSolutionState] = useState(initSolutionState);
+
+ const handleSolutionModalHide = () => setSolutionState(initSolutionState);
+
+ const getProjectSolution = (projectId, projectTitle) => {
+ const {
+ user: { completedChallenges }
+ } = props;
+
+ const completedProject = find(
+ completedChallenges,
+ ({ id }) => projectId === id
+ );
+
+ if (!completedProject) {
+ return null;
+ }
+
+ const { solution, githubLink, files } = completedProject;
+ const onClickHandler = () =>
+ setSolutionState({
+ projectTitle,
+ files,
+ solution,
+ isOpen: true
+ });
+
+ if (files && files.length) {
+ return (
+
+ solution
+
+ );
+ }
+ if (githubLink) {
+ return (
+ <>
+
+ solution
+
+ ,{' '}
+
+ source
+
+ >
+ );
+ }
+ if (maybeUrlRE.test(solution)) {
+ return (
+
+ solution
+
+ );
+ }
+ return (
+
+ solution
+
+ );
+ };
+
+ const renderProjectsFor = certName => {
+ if (certName === 'Legacy Full Stack') {
+ const legacyCerts = [
+ { title: 'Responsive Web Design' },
+ { title: 'JavaScript Algorithms and Data Structures' },
+ { title: 'Front End Libraries' },
+ { title: 'Data Visualization' },
+ { title: 'APIs and Microservices' },
+ { title: 'Legacy Information Security and Quality Assurance' }
+ ];
+ return legacyCerts.map((cert, ind) => {
+ const mapToUse = projectMap[cert.title] || legacyProjectMap[cert.title];
+ const { superBlock } = first(mapToUse);
+ const certLocation = `/certification/${username}/${superBlock}`;
+ return (
+
+
+ {cert.title}
+
+
+ );
+ });
+ }
+ return (projectMap[certName] || legacyProjectMap[certName]).map(
+ ({ link, title, id }) => (
+
+
+ {title}
+
+ : {getProjectSolution(id, title)}
+
+ )
+ );
+ };
+
+ const {
+ certName,
+ name,
+ user: { username }
+ } = props;
+ const { files, isOpen, projectTitle, solution } = solutionState;
+ return (
+
+ {certName === 'Legacy Full Stack'
+ ? `As part of this Legacy Full Stack certification, ${name} completed the following certifications:`
+ : `As part of this certification, ${name} built the following projects and got all automated test suites to pass:`}
+
+
{renderProjectsFor(certName)}
+
+ {isOpen ? (
+
+ ) : null}
+ If you suspect that any of these projects violate the{' '}
+
+ academic honesty policy
+
+ , please{' '}
+
+ report this to our team
+
+ .
+
+ );
+};
+
+ShowProjectLinks.propTypes = propTypes;
+ShowProjectLinks.displayName = 'ShowProjectLinks';
+
+export default ShowProjectLinks;
diff --git a/client/src/components/SolutionViewer/ProjectModal.js b/client/src/components/SolutionViewer/ProjectModal.js
new file mode 100644
index 0000000000..7f616e02ae
--- /dev/null
+++ b/client/src/components/SolutionViewer/ProjectModal.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import SolutionViewer from './SolutionViewer';
+import { Button, Modal } from '@freecodecamp/react-bootstrap';
+
+const propTypes = {
+ files: PropTypes.arrayOf(
+ PropTypes.shape({
+ contents: PropTypes.string,
+ ext: PropTypes.string,
+ key: PropTypes.string,
+ name: PropTypes.string,
+ path: PropTypes.string
+ })
+ ),
+ handleSolutionModalHide: PropTypes.func,
+ isOpen: PropTypes.bool,
+ projectTitle: PropTypes.string,
+ solution: PropTypes.string,
+ t: PropTypes.func.isRequired
+};
+
+const ProjectModal = props => {
+ const {
+ isOpen,
+ projectTitle,
+ files,
+ solution,
+ t,
+ handleSolutionModalHide
+ } = props;
+ return (
+
+
+
+ {t('settings.labels.solution-for', {
+ projectTitle: projectTitle
+ })}
+
+
+
+
+
+
+ {t('buttons.close')}
+
+
+ );
+};
+
+ProjectModal.propTypes = propTypes;
+ProjectModal.displayName = 'ProjectModal';
+
+export default ProjectModal;
diff --git a/client/src/components/settings/SolutionViewer.js b/client/src/components/SolutionViewer/SolutionViewer.js
similarity index 100%
rename from client/src/components/settings/SolutionViewer.js
rename to client/src/components/SolutionViewer/SolutionViewer.js
diff --git a/client/src/components/layouts/project-links.css b/client/src/components/layouts/project-links.css
new file mode 100644
index 0000000000..b7852e43d3
--- /dev/null
+++ b/client/src/components/layouts/project-links.css
@@ -0,0 +1,16 @@
+.project-link-button-override {
+ all: unset;
+ color: inherit;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.project-link-button-override:hover {
+ text-decoration: none;
+ color: var(--tertiary-color);
+ background-color: var(--tertiary-background);
+}
+
+.project-link {
+ font-weight: 800;
+}
diff --git a/client/src/components/profile/components/TimeLine.js b/client/src/components/profile/components/TimeLine.js
index f992cedcfb..4287fe9c2d 100644
--- a/client/src/components/profile/components/TimeLine.js
+++ b/client/src/components/profile/components/TimeLine.js
@@ -14,7 +14,7 @@ import { withTranslation } from 'react-i18next';
import './timeline.css';
import TimelinePagination from './TimelinePagination';
import { FullWidthRow, Link } from '../../helpers';
-import SolutionViewer from '../../settings/SolutionViewer';
+import SolutionViewer from '../../SolutionViewer/SolutionViewer';
import {
getCertIds,
getPathFromID,
diff --git a/client/src/components/settings/Certification.js b/client/src/components/settings/Certification.js
index c724d565c0..5a8973febb 100644
--- a/client/src/components/settings/Certification.js
+++ b/client/src/components/settings/Certification.js
@@ -7,8 +7,7 @@ import {
Table,
Button,
DropdownButton,
- MenuItem,
- Modal
+ MenuItem
} from '@freecodecamp/react-bootstrap';
import { Link, navigate } from 'gatsby';
import { createSelector } from 'reselect';
@@ -20,7 +19,7 @@ import {
} from '../../resources/certAndProjectMap';
import SectionHeader from './SectionHeader';
-import SolutionViewer from './SolutionViewer';
+import ProjectModal from '../SolutionViewer/ProjectModal';
import { FullWidthRow, Spacer } from '../helpers';
import { Form } from '../formHelpers';
@@ -599,28 +598,14 @@ export class CertificationSettings extends Component {
{this.renderLegacyFullStack()}
{legacyCertifications.map(this.renderLegacyCertifications)}
{isOpen ? (
-
-
-
- {t('settings.labels.solution-for', {
- projectTitle: projectTitle
- })}
-
-
-
-
-
-
-
- {t('buttons.close')}
-
-
-
+
) : null}
);