refactor: display solutions (#45139)
* refactor: re-organise show-project-links * refactor: update ChallengeFile's declared shape * fix: handle missing challenge solution * refactor: use display function for Certification * refactor: use display function for TimeLine * refactor: use common component for timeline + cert * fix: handle legacy solutions * refactor: use widget for certifications * refactor: reorganise ShowDisplayWidget * refactor: remove unused ids * test: pass dataCy, not projectTitle, to widget * chore: kebabify * revert: add id back for dropdown Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * revert: add the ids back Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
committed by
GitHub
parent
92778f1b2f
commit
b223cdd255
@ -551,6 +551,7 @@
|
|||||||
"heading-legacy-full-stack": "As part of this Legacy Full Stack certification, {{user}} completed the following certifications:",
|
"heading-legacy-full-stack": "As part of this Legacy Full Stack certification, {{user}} completed the following certifications:",
|
||||||
"heading": "As part of this certification, {{user}} built the following projects and got all automated test suites to pass:",
|
"heading": "As part of this certification, {{user}} built the following projects and got all automated test suites to pass:",
|
||||||
"solution": "solution",
|
"solution": "solution",
|
||||||
|
"no-solution": "error displaying solution, email support@freeCodeCamp.org to get help.",
|
||||||
"source": "source",
|
"source": "source",
|
||||||
"footnote": "If you suspect that any of these projects violate the <2>academic honesty policy</2>, please <5>report this to our team</5>.",
|
"footnote": "If you suspect that any of these projects violate the <2>academic honesty policy</2>, please <5>report this to our team</5>.",
|
||||||
"title": {
|
"title": {
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
legacyProjectMap
|
legacyProjectMap
|
||||||
} from '../resources/cert-and-project-map';
|
} from '../resources/cert-and-project-map';
|
||||||
|
|
||||||
import { maybeUrlRE } from '../utils';
|
import { SolutionDisplayWidget } from '../components/solution-display-widget';
|
||||||
|
|
||||||
interface ShowProjectLinksProps {
|
interface ShowProjectLinksProps {
|
||||||
certName: string;
|
certName: string;
|
||||||
@ -52,8 +52,8 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { solution, githubLink, challengeFiles } = completedProject;
|
const { solution, challengeFiles } = completedProject;
|
||||||
const onClickHandler = () =>
|
const showFilesSolution = () =>
|
||||||
setSolutionState({
|
setSolutionState({
|
||||||
projectTitle,
|
projectTitle,
|
||||||
challengeFiles,
|
challengeFiles,
|
||||||
@ -61,46 +61,13 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => {
|
|||||||
isOpen: true
|
isOpen: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (challengeFiles?.length) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className='project-link-button-override'
|
|
||||||
data-cy={`${projectTitle} solution`}
|
|
||||||
onClick={onClickHandler}
|
|
||||||
>
|
|
||||||
{t('certification.project.solution')}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (githubLink) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a href={solution ?? ''} rel='noopener noreferrer' target='_blank'>
|
|
||||||
{t('certification.project.solution')}
|
|
||||||
</a>
|
|
||||||
,{' '}
|
|
||||||
<a href={githubLink} rel='noopener noreferrer' target='_blank'>
|
|
||||||
{t('certification.project.source')}
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (maybeUrlRE.test(solution ?? '')) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className='btn-invert'
|
|
||||||
href={solution ?? ''}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('certification.project.solution')}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<button className='project-link-button-override' onClick={onClickHandler}>
|
<SolutionDisplayWidget
|
||||||
{t('certification.project.solution')}
|
completedChallenge={completedProject}
|
||||||
</button>
|
dataCy={`${projectTitle} solution`}
|
||||||
|
displayContext='certification'
|
||||||
|
showFilesSolution={showFilesSolution}
|
||||||
|
></SolutionDisplayWidget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { createSelector } from 'reselect';
|
|||||||
import envData from '../../../config/env.json';
|
import envData from '../../../config/env.json';
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
import { Loader, Spacer } from '../components/helpers';
|
import { Loader, Spacer } from '../components/helpers';
|
||||||
import Certification from '../components/settings/Certification';
|
import Certification from '../components/settings/certification';
|
||||||
import About from '../components/settings/about';
|
import About from '../components/settings/about';
|
||||||
import DangerZone from '../components/settings/danger-zone';
|
import DangerZone from '../components/settings/danger-zone';
|
||||||
import Email from '../components/settings/email';
|
import Email from '../components/settings/email';
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { useStaticQuery } from 'gatsby';
|
import { useStaticQuery } from 'gatsby';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TimeLine from './TimeLine';
|
import TimeLine from './time-line';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
import {
|
import { Button, Modal, Table } from '@freecodecamp/react-bootstrap';
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
Table,
|
|
||||||
DropdownButton,
|
|
||||||
MenuItem
|
|
||||||
} from '@freecodecamp/react-bootstrap';
|
|
||||||
import Loadable from '@loadable/component';
|
import Loadable from '@loadable/component';
|
||||||
import { useStaticQuery, graphql } from 'gatsby';
|
import { useStaticQuery, graphql } from 'gatsby';
|
||||||
import { reverse, sortBy } from 'lodash-es';
|
import { reverse, sortBy } from 'lodash-es';
|
||||||
@ -21,8 +15,8 @@ import {
|
|||||||
} from '../../../../../utils';
|
} from '../../../../../utils';
|
||||||
import CertificationIcon from '../../../assets/icons/certification-icon';
|
import CertificationIcon from '../../../assets/icons/certification-icon';
|
||||||
import { ChallengeFiles, CompletedChallenge } from '../../../redux/prop-types';
|
import { ChallengeFiles, CompletedChallenge } from '../../../redux/prop-types';
|
||||||
import { maybeUrlRE } from '../../../utils';
|
|
||||||
import { FullWidthRow, Link } from '../../helpers';
|
import { FullWidthRow, Link } from '../../helpers';
|
||||||
|
import { SolutionDisplayWidget } from '../../solution-display-widget';
|
||||||
import TimelinePagination from './timeline-pagination';
|
import TimelinePagination from './timeline-pagination';
|
||||||
|
|
||||||
import './timeline.css';
|
import './timeline.css';
|
||||||
@ -53,7 +47,6 @@ function TimelineInner({
|
|||||||
idToNameMap,
|
idToNameMap,
|
||||||
sortedTimeline,
|
sortedTimeline,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
|
||||||
completedMap,
|
completedMap,
|
||||||
t,
|
t,
|
||||||
username
|
username
|
||||||
@ -96,73 +89,20 @@ function TimelineInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderViewButton(
|
function renderViewButton(
|
||||||
id: string,
|
completedChallenge: CompletedChallenge
|
||||||
challengeFiles: ChallengeFiles,
|
|
||||||
githubLink?: string,
|
|
||||||
solution?: string | null
|
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
if (challengeFiles?.length) {
|
const { id, solution, challengeFiles } = completedChallenge;
|
||||||
return (
|
return (
|
||||||
<Button
|
<SolutionDisplayWidget
|
||||||
block={true}
|
completedChallenge={completedChallenge}
|
||||||
bsStyle='primary'
|
showFilesSolution={() => viewSolution(id, solution, challengeFiles)}
|
||||||
className='btn-invert'
|
displayContext={'timeline'}
|
||||||
id={`btn-for-${id}`}
|
></SolutionDisplayWidget>
|
||||||
onClick={() => viewSolution(id, solution, challengeFiles)}
|
);
|
||||||
>
|
|
||||||
{t('buttons.show-code')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else if (githubLink) {
|
|
||||||
return (
|
|
||||||
<div className='solutions-dropdown'>
|
|
||||||
<DropdownButton
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
id={`dropdown-for-${id}`}
|
|
||||||
title='View'
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
bsStyle='primary'
|
|
||||||
href={solution}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.frontend')}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
bsStyle='primary'
|
|
||||||
href={githubLink}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.backend')}
|
|
||||||
</MenuItem>
|
|
||||||
</DropdownButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (solution && maybeUrlRE.test(solution)) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
href={solution}
|
|
||||||
id={`btn-for-${id}`}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.view')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCompletion(completed: CompletedChallenge): JSX.Element {
|
function renderCompletion(completed: CompletedChallenge): JSX.Element {
|
||||||
const { id, challengeFiles, githubLink, solution } = completed;
|
const { id } = completed;
|
||||||
const completedDate = new Date(completed.completedDate);
|
const completedDate = new Date(completed.completedDate);
|
||||||
// @ts-expect-error idToNameMap is not a <string, string> Map...
|
// @ts-expect-error idToNameMap is not a <string, string> Map...
|
||||||
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
|
const { challengeTitle, challengePath, certPath } = idToNameMap.get(id);
|
||||||
@ -181,7 +121,7 @@ function TimelineInner({
|
|||||||
<Link to={challengePath as string}>{challengeTitle}</Link>
|
<Link to={challengePath as string}>{challengeTitle}</Link>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{renderViewButton(id, challengeFiles, githubLink, solution)}</td>
|
<td>{renderViewButton(completed)}</td>
|
||||||
<td className='text-center'>
|
<td className='text-center'>
|
||||||
<time dateTime={completedDate.toISOString()}>
|
<time dateTime={completedDate.toISOString()}>
|
||||||
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
{completedDate.toLocaleString([localeCode, 'en-US'], {
|
@ -5,7 +5,7 @@ import { TFunction, useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { FullWidthRow, Link, Spacer } from '../helpers';
|
import { FullWidthRow, Link, Spacer } from '../helpers';
|
||||||
import { User } from './../../redux/prop-types';
|
import { User } from './../../redux/prop-types';
|
||||||
import Timeline from './components/TimeLine';
|
import Timeline from './components/time-line';
|
||||||
import Camper from './components/camper';
|
import Camper from './components/camper';
|
||||||
import Certifications from './components/certifications';
|
import Certifications from './components/certifications';
|
||||||
import HeatMap from './components/heat-map';
|
import HeatMap from './components/heat-map';
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { Table, Button } from '@freecodecamp/react-bootstrap';
|
||||||
Table,
|
|
||||||
Button,
|
|
||||||
DropdownButton,
|
|
||||||
MenuItem
|
|
||||||
} from '@freecodecamp/react-bootstrap';
|
|
||||||
import { Link, navigate } from 'gatsby';
|
import { Link, navigate } from 'gatsby';
|
||||||
import { find, first } from 'lodash-es';
|
import { find, first } from 'lodash-es';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -16,12 +11,10 @@ import {
|
|||||||
projectMap,
|
projectMap,
|
||||||
legacyProjectMap
|
legacyProjectMap
|
||||||
} from '../../resources/cert-and-project-map';
|
} from '../../resources/cert-and-project-map';
|
||||||
|
|
||||||
import { maybeUrlRE } from '../../utils';
|
|
||||||
import { FlashMessages } from '../Flash/redux/flash-messages';
|
import { FlashMessages } from '../Flash/redux/flash-messages';
|
||||||
import ProjectModal from '../SolutionViewer/ProjectModal';
|
import ProjectModal from '../SolutionViewer/ProjectModal';
|
||||||
import { FullWidthRow, Spacer } from '../helpers';
|
import { FullWidthRow, Spacer } from '../helpers';
|
||||||
|
import { SolutionDisplayWidget } from '../solution-display-widget';
|
||||||
import SectionHeader from './section-header';
|
import SectionHeader from './section-header';
|
||||||
|
|
||||||
import './certification.css';
|
import './certification.css';
|
||||||
@ -163,7 +156,7 @@ export class CertificationSettings extends Component {
|
|||||||
getUserIsCertMap = () => isCertMapSelector(this.props);
|
getUserIsCertMap = () => isCertMapSelector(this.props);
|
||||||
|
|
||||||
getProjectSolution = (projectId, projectTitle) => {
|
getProjectSolution = (projectId, projectTitle) => {
|
||||||
const { completedChallenges, t } = this.props;
|
const { completedChallenges } = this.props;
|
||||||
const completedProject = find(
|
const completedProject = find(
|
||||||
completedChallenges,
|
completedChallenges,
|
||||||
({ id }) => projectId === id
|
({ id }) => projectId === id
|
||||||
@ -171,7 +164,7 @@ export class CertificationSettings extends Component {
|
|||||||
if (!completedProject) {
|
if (!completedProject) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { solution, githubLink, challengeFiles } = completedProject;
|
const { solution, challengeFiles } = completedProject;
|
||||||
const onClickHandler = () =>
|
const onClickHandler = () =>
|
||||||
this.setState({
|
this.setState({
|
||||||
solutionViewer: {
|
solutionViewer: {
|
||||||
@ -181,75 +174,14 @@ export class CertificationSettings extends Component {
|
|||||||
isOpen: true
|
isOpen: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (challengeFiles?.length) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
data-cy={projectTitle}
|
|
||||||
id={`btn-for-${projectId}`}
|
|
||||||
onClick={onClickHandler}
|
|
||||||
>
|
|
||||||
{t('buttons.show-code')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (githubLink) {
|
|
||||||
return (
|
|
||||||
<div className='solutions-dropdown'>
|
|
||||||
<DropdownButton
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
id={`dropdown-for-${projectId}`}
|
|
||||||
title='Show Solutions'
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
bsStyle='primary'
|
|
||||||
href={solution}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.frontend')}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
bsStyle='primary'
|
|
||||||
href={githubLink}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.backend')}
|
|
||||||
</MenuItem>
|
|
||||||
</DropdownButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (maybeUrlRE.test(solution)) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-invert'
|
|
||||||
href={solution}
|
|
||||||
id={`btn-for-${projectId}`}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
{t('buttons.show-solution')}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<SolutionDisplayWidget
|
||||||
block={true}
|
completedChallenge={completedProject}
|
||||||
bsStyle='primary'
|
dataCy={projectTitle}
|
||||||
className='btn-invert'
|
showFilesSolution={onClickHandler}
|
||||||
id={`btn-for-${projectId}`}
|
displayContext={'settings'}
|
||||||
onClick={onClickHandler}
|
></SolutionDisplayWidget>
|
||||||
>
|
|
||||||
{t('buttons.show-code')}
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { createStore } from '../../redux/createStore';
|
import { createStore } from '../../redux/createStore';
|
||||||
|
|
||||||
import { CertificationSettings } from './Certification';
|
import { CertificationSettings } from './certification';
|
||||||
|
|
||||||
jest.mock('../../analytics');
|
jest.mock('../../analytics');
|
||||||
|
|
141
client/src/components/solution-display-widget/index.tsx
Normal file
141
client/src/components/solution-display-widget/index.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownButton,
|
||||||
|
MenuItem
|
||||||
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CompletedChallenge } from '../../redux/prop-types';
|
||||||
|
import { getSolutionDisplayType } from '../../utils/solution-display-type';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
completedChallenge: CompletedChallenge;
|
||||||
|
dataCy?: string;
|
||||||
|
showFilesSolution: () => void;
|
||||||
|
displayContext: 'timeline' | 'settings' | 'certification';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SolutionDisplayWidget({
|
||||||
|
completedChallenge,
|
||||||
|
dataCy,
|
||||||
|
showFilesSolution,
|
||||||
|
displayContext
|
||||||
|
}: Props) {
|
||||||
|
const { id, solution, githubLink } = completedChallenge;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const dropdownTitle =
|
||||||
|
displayContext === 'settings' ? 'Show Solutions' : 'View';
|
||||||
|
const projectLinkText =
|
||||||
|
displayContext === 'settings'
|
||||||
|
? t('buttons.show-solution')
|
||||||
|
: t('buttons.view');
|
||||||
|
|
||||||
|
const ShowFilesSolutionForCertification = (
|
||||||
|
<button
|
||||||
|
className='project-link-button-override'
|
||||||
|
data-cy={dataCy}
|
||||||
|
onClick={showFilesSolution}
|
||||||
|
>
|
||||||
|
{t('certification.project.solution')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
const ShowProjectAndGithubLinkForCertification = (
|
||||||
|
<>
|
||||||
|
<a href={solution ?? ''} rel='noopener noreferrer' target='_blank'>
|
||||||
|
{t('certification.project.solution')}
|
||||||
|
</a>
|
||||||
|
,{' '}
|
||||||
|
<a href={githubLink} rel='noopener noreferrer' target='_blank'>
|
||||||
|
{t('certification.project.source')}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const ShowProjectLinkForCertification = (
|
||||||
|
<a
|
||||||
|
className='btn-invert'
|
||||||
|
href={solution ?? ''}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{t('certification.project.solution')}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
const MissingSolutionComponentForCertification = (
|
||||||
|
<>{t('certification.project.no-solution')}</>
|
||||||
|
);
|
||||||
|
const ShowFilesSolution = (
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-invert'
|
||||||
|
data-cy={dataCy}
|
||||||
|
id={`btn-for-${id}`}
|
||||||
|
onClick={showFilesSolution}
|
||||||
|
>
|
||||||
|
{t('buttons.show-code')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const ShowProjectAndGithubLinks = (
|
||||||
|
<div className='solutions-dropdown'>
|
||||||
|
<DropdownButton
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-invert'
|
||||||
|
id={`dropdown-for-${id}`}
|
||||||
|
title={dropdownTitle}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
bsStyle='primary'
|
||||||
|
href={solution}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{t('buttons.frontend')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
bsStyle='primary'
|
||||||
|
href={githubLink}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{t('buttons.backend')}
|
||||||
|
</MenuItem>
|
||||||
|
</DropdownButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const ShowProjectLink = (
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-invert'
|
||||||
|
href={solution}
|
||||||
|
id={`btn-for-${id}`}
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{projectLinkText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const MissingSolutionComponent =
|
||||||
|
displayContext === 'settings' ? (
|
||||||
|
<>{t('certification.project.no-solution')}</>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const displayComponents =
|
||||||
|
displayContext === 'certification'
|
||||||
|
? {
|
||||||
|
showFilesSolution: ShowFilesSolutionForCertification,
|
||||||
|
showProjectAndGitHubLinks: ShowProjectAndGithubLinkForCertification,
|
||||||
|
showProjectLink: ShowProjectLinkForCertification,
|
||||||
|
none: MissingSolutionComponentForCertification
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
showFilesSolution: ShowFilesSolution,
|
||||||
|
showProjectAndGitHubLinks: ShowProjectAndGithubLinks,
|
||||||
|
showProjectLink: ShowProjectLink,
|
||||||
|
none: MissingSolutionComponent
|
||||||
|
};
|
||||||
|
|
||||||
|
return displayComponents[getSolutionDisplayType(completedChallenge)];
|
||||||
|
}
|
@ -363,7 +363,7 @@ export type ChallengeFile = {
|
|||||||
seed: string;
|
seed: string;
|
||||||
contents: string;
|
contents: string;
|
||||||
id: string;
|
id: string;
|
||||||
history: [[string], string];
|
history: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChallengeFiles = ChallengeFile[] | null;
|
export type ChallengeFiles = ChallengeFile[] | null;
|
||||||
|
39
client/src/utils/__fixtures/completed-challenges.ts
Normal file
39
client/src/utils/__fixtures/completed-challenges.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {challengeFiles} from '../../../../utils/__fixtures__/challenges';
|
||||||
|
|
||||||
|
const baseChallenge = {
|
||||||
|
id: '1',
|
||||||
|
completedDate: 1,
|
||||||
|
challengeFiles: []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onlySolution = {
|
||||||
|
...baseChallenge,
|
||||||
|
solution: 'https://some-url.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const legacySolution = {
|
||||||
|
...baseChallenge,
|
||||||
|
solution: 'var x = 1;'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bothLinks = {
|
||||||
|
...baseChallenge,
|
||||||
|
githubLink: 'https://some.thing',
|
||||||
|
solution: 'https://some-url.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withChallenges = {
|
||||||
|
...bothLinks,
|
||||||
|
challengeFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onlyGithubLink = {
|
||||||
|
...baseChallenge,
|
||||||
|
githubLink: 'https://some.thing'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invalidGithubLink = {
|
||||||
|
...baseChallenge,
|
||||||
|
githubLink: 'something',
|
||||||
|
solution: 'https://some-url.com'
|
||||||
|
}
|
29
client/src/utils/solution-display-type.test.ts
Normal file
29
client/src/utils/solution-display-type.test.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
bothLinks,
|
||||||
|
legacySolution,
|
||||||
|
invalidGithubLink,
|
||||||
|
onlyGithubLink,
|
||||||
|
onlySolution,
|
||||||
|
withChallenges
|
||||||
|
} from './__fixtures/completed-challenges';
|
||||||
|
import { getSolutionDisplayType } from './solution-display-type';
|
||||||
|
|
||||||
|
describe('getSolutionDisplayType', () => {
|
||||||
|
it('should handle missing solutions', () => {
|
||||||
|
expect(getSolutionDisplayType(onlyGithubLink)).toBe('none');
|
||||||
|
});
|
||||||
|
it('should handle legacy solutions', () => {
|
||||||
|
expect(getSolutionDisplayType(legacySolution)).toBe('showFilesSolution');
|
||||||
|
});
|
||||||
|
it('should handle solutions with files', () => {
|
||||||
|
expect(getSolutionDisplayType(withChallenges)).toBe('showFilesSolution');
|
||||||
|
});
|
||||||
|
it('should handle solutions with a single valid url', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
expect(getSolutionDisplayType(onlySolution)).toBe('showProjectLink');
|
||||||
|
expect(getSolutionDisplayType(invalidGithubLink)).toBe('showProjectLink');
|
||||||
|
});
|
||||||
|
it('should handle solutions with both links', () => {
|
||||||
|
expect(getSolutionDisplayType(bothLinks)).toBe('showProjectAndGitHubLinks');
|
||||||
|
});
|
||||||
|
});
|
16
client/src/utils/solution-display-type.ts
Normal file
16
client/src/utils/solution-display-type.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { CompletedChallenge } from '../redux/prop-types';
|
||||||
|
import { maybeUrlRE } from '.';
|
||||||
|
|
||||||
|
export const getSolutionDisplayType = ({
|
||||||
|
solution,
|
||||||
|
githubLink,
|
||||||
|
challengeFiles
|
||||||
|
}: CompletedChallenge) => {
|
||||||
|
if (challengeFiles?.length) return 'showFilesSolution';
|
||||||
|
if (!solution) return 'none';
|
||||||
|
// Some of the user records still have JavaScript project solutions stored as
|
||||||
|
// solution strings
|
||||||
|
if (!maybeUrlRE.test(solution)) return 'showFilesSolution';
|
||||||
|
if (maybeUrlRE.test(githubLink ?? '')) return 'showProjectAndGitHubLinks';
|
||||||
|
return 'showProjectLink';
|
||||||
|
};
|
@ -18,7 +18,7 @@ const fileJoi = Joi.object().keys({
|
|||||||
seed: Joi.string().allow(''),
|
seed: Joi.string().allow(''),
|
||||||
contents: Joi.string().allow(''),
|
contents: Joi.string().allow(''),
|
||||||
id: Joi.string().allow(''),
|
id: Joi.string().allow(''),
|
||||||
history: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')]
|
history: Joi.array().items(Joi.string().allow(''))
|
||||||
});
|
});
|
||||||
|
|
||||||
const schema = Joi.object()
|
const schema = Joi.object()
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
exports.challengeFiles = [
|
import { ChallengeFile } from "../../client/src/redux/prop-types";
|
||||||
|
|
||||||
|
export const challengeFiles: ChallengeFile[] = [
|
||||||
{
|
{
|
||||||
|
id: '1',
|
||||||
contents: 'some css',
|
contents: 'some css',
|
||||||
error: null,
|
error: null,
|
||||||
ext: 'css',
|
ext: 'css',
|
||||||
@ -7,11 +10,13 @@ exports.challengeFiles = [
|
|||||||
history: ['styles.css'],
|
history: ['styles.css'],
|
||||||
fileKey: 'stylescss',
|
fileKey: 'stylescss',
|
||||||
name: 'styles',
|
name: 'styles',
|
||||||
path: 'styles.css',
|
|
||||||
seed: 'some css',
|
seed: 'some css',
|
||||||
tail: ''
|
tail: '',
|
||||||
|
editableRegionBoundaries: [],
|
||||||
|
usesMultifileEditor: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: '2',
|
||||||
contents: 'some html',
|
contents: 'some html',
|
||||||
error: null,
|
error: null,
|
||||||
ext: 'html',
|
ext: 'html',
|
||||||
@ -19,11 +24,13 @@ exports.challengeFiles = [
|
|||||||
history: ['index.html'],
|
history: ['index.html'],
|
||||||
fileKey: 'indexhtml',
|
fileKey: 'indexhtml',
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: 'index.html',
|
|
||||||
seed: 'some html',
|
seed: 'some html',
|
||||||
tail: ''
|
tail: '',
|
||||||
|
editableRegionBoundaries: [],
|
||||||
|
usesMultifileEditor: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: '3',
|
||||||
contents: 'some js',
|
contents: 'some js',
|
||||||
error: null,
|
error: null,
|
||||||
ext: 'js',
|
ext: 'js',
|
||||||
@ -31,11 +38,13 @@ exports.challengeFiles = [
|
|||||||
history: ['script.js'],
|
history: ['script.js'],
|
||||||
fileKey: 'scriptjs',
|
fileKey: 'scriptjs',
|
||||||
name: 'script',
|
name: 'script',
|
||||||
path: 'script.js',
|
|
||||||
seed: 'some js',
|
seed: 'some js',
|
||||||
tail: ''
|
tail: '',
|
||||||
|
editableRegionBoundaries: [],
|
||||||
|
usesMultifileEditor: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: '4',
|
||||||
contents: 'some jsx',
|
contents: 'some jsx',
|
||||||
error: null,
|
error: null,
|
||||||
ext: 'jsx',
|
ext: 'jsx',
|
||||||
@ -43,8 +52,9 @@ exports.challengeFiles = [
|
|||||||
history: ['index.jsx'],
|
history: ['index.jsx'],
|
||||||
fileKey: 'indexjsx',
|
fileKey: 'indexjsx',
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: 'index.jsx',
|
|
||||||
seed: 'some jsx',
|
seed: 'some jsx',
|
||||||
tail: ''
|
tail: '',
|
||||||
|
editableRegionBoundaries: [],
|
||||||
|
usesMultifileEditor: true,
|
||||||
}
|
}
|
||||||
];
|
]
|
Reference in New Issue
Block a user