feat(client): add project links to certificate (#40071)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Shaun Hamilton
2021-02-01 13:25:14 +00:00
committed by GitHub
parent 5503b54f85
commit 5539dbf086
7 changed files with 345 additions and 33 deletions

View File

@ -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 => {
</Row>
</Grid>
{signedInUserName === username ? shareCertBtns : ''}
<Spacer size={2} />
<ShowProjectLinks user={user} name={userFullName} certName={certTitle} />
</div>
);
};

View File

@ -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 (
<button
onClick={onClickHandler}
className='project-link-button-override'
>
solution
</button>
);
}
if (githubLink) {
return (
<>
<a href={solution} rel='noopener noreferrer' target='_blank'>
solution
</a>
,{' '}
<a href={githubLink} rel='noopener noreferrer' target='_blank'>
source
</a>
</>
);
}
if (maybeUrlRE.test(solution)) {
return (
<a
block={'true'}
className='btn-invert'
href={solution}
rel='noopener noreferrer'
target='_blank'
>
solution
</a>
);
}
return (
<button className='project-link-button-override' onClick={onClickHandler}>
solution
</button>
);
};
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 (
<li key={ind}>
<a
href={certLocation}
className='btn-invert project-link'
rel='noopener noreferrer'
target='_blank'
>
{cert.title}
</a>
</li>
);
});
}
return (projectMap[certName] || legacyProjectMap[certName]).map(
({ link, title, id }) => (
<li key={id}>
<Link to={link} className='project-link'>
{title}
</Link>
: {getProjectSolution(id, title)}
</li>
)
);
};
const {
certName,
name,
user: { username }
} = props;
const { files, isOpen, projectTitle, solution } = solutionState;
return (
<div>
{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:`}
<Spacer />
<ul>{renderProjectsFor(certName)}</ul>
<Spacer />
{isOpen ? (
<ProjectModal
files={files}
handleSolutionModalHide={handleSolutionModalHide}
isOpen={isOpen}
projectTitle={projectTitle}
solution={solution}
/>
) : null}
If you suspect that any of these projects violate the{' '}
<a
href='https://www.freecodecamp.org/news/academic-honesty-policy/'
target='_blank'
rel='noreferrer'
>
academic honesty policy
</a>
, please{' '}
<a
href={`/user/${username}/report-user`}
target='_blank'
rel='noreferrer'
>
report this to our team
</a>
.
</div>
);
};
ShowProjectLinks.propTypes = propTypes;
ShowProjectLinks.displayName = 'ShowProjectLinks';
export default ShowProjectLinks;

View File

@ -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 (
<Modal
aria-labelledby='solution-viewer-modal-title'
bsSize='large'
onHide={handleSolutionModalHide}
show={isOpen}
>
<Modal.Header className='this-one?' closeButton={true}>
<Modal.Title id='solution-viewer-modal-title'>
{t('settings.labels.solution-for', {
projectTitle: projectTitle
})}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SolutionViewer files={files} solution={solution} />
</Modal.Body>
<Modal.Footer>
<Button onClick={handleSolutionModalHide}>{t('buttons.close')}</Button>
</Modal.Footer>
</Modal>
);
};
ProjectModal.propTypes = propTypes;
ProjectModal.displayName = 'ProjectModal';
export default ProjectModal;

View File

@ -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;
}

View File

@ -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,

View File

@ -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 ? (
<Modal
aria-labelledby='solution-viewer-modal-title'
bsSize='large'
onHide={this.handleSolutionModalHide}
show={isOpen}
>
<Modal.Header className='this-one?' closeButton={true}>
<Modal.Title id='solution-viewer-modal-title'>
{t('settings.labels.solution-for', {
projectTitle: projectTitle
})}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SolutionViewer files={files} solution={solution} />
</Modal.Body>
<Modal.Footer>
<Button onClick={this.handleSolutionModalHide}>
{t('buttons.close')}
</Button>
</Modal.Footer>
</Modal>
<ProjectModal
files={files}
handleSolutionModalHide={this.handleSolutionModalHide}
isOpen={isOpen}
projectTitle={projectTitle}
solution={solution}
t={t}
/>
) : null}
</section>
);