fix(learn): split and simplified learn map (#39154)
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
48c97238fc
commit
ac3d762bb5
@ -116,12 +116,12 @@ class ShowUser extends Component {
|
|||||||
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
|
<title>{t('report.portfolio')} | freeCodeCamp.org</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Spacer size={2} />
|
<Spacer size={2} />
|
||||||
<Row className='text-center'>
|
<Row className='text-center overflow-fix'>
|
||||||
<Col sm={8} smOffset={2} xs={12}>
|
<Col sm={8} smOffset={2} xs={12}>
|
||||||
<h2>{t('report.portfolio-2', { username: username })}</h2>
|
<h2>{t('report.portfolio-2', { username: username })}</h2>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row className='overflow-fix'>
|
||||||
<Col sm={6} smOffset={3} xs={12}>
|
<Col sm={6} smOffset={3} xs={12}>
|
||||||
<p>
|
<p>
|
||||||
<Trans email={email} i18nKey='report.notify-1'>
|
<Trans email={email} i18nKey='report.notify-1'>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
position: relative;
|
flex-shrink: 0;
|
||||||
color: var(--tertiary-color);
|
color: var(--tertiary-color);
|
||||||
background: var(--tertiary-background);
|
background: var(--tertiary-background);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
@ -1,145 +1,29 @@
|
|||||||
/* global expect jest */
|
/* global expect jest */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useStaticQuery } from 'gatsby';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { createStore } from '../../redux/createStore';
|
|
||||||
|
|
||||||
import { Map } from './';
|
import { Map } from './';
|
||||||
import mockChallengeNodes from '../../__mocks__/challenge-nodes';
|
import mockChallengeNodes from '../../__mocks__/challenge-nodes';
|
||||||
import mockIntroNodes from '../../__mocks__/intro-nodes';
|
|
||||||
|
|
||||||
import { dasherize } from '../../../../utils/slugs';
|
beforeEach(() => {
|
||||||
|
useStaticQuery.mockImplementationOnce(() => ({
|
||||||
function renderWithRedux(ui) {
|
allChallengeNode: {
|
||||||
return render(<Provider store={createStore()}>{ui}</Provider>);
|
nodes: mockChallengeNodes
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
const baseProps = {
|
});
|
||||||
introNodes: mockIntroNodes,
|
|
||||||
nodes: mockChallengeNodes,
|
|
||||||
toggleBlock: () => {},
|
|
||||||
toggleSuperBlock: () => {},
|
|
||||||
resetExpansion: () => {},
|
|
||||||
isSignedIn: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// set .scrollTo to avoid errors in default test environment
|
// set .scrollTo to avoid errors in default test environment
|
||||||
window.scrollTo = jest.fn();
|
window.scrollTo = jest.fn();
|
||||||
|
|
||||||
test('<Map /> snapshot', () => {
|
test('<Map /> snapshot', () => {
|
||||||
const { container } = renderWithRedux(
|
const { container } = render(<Map {...props} />);
|
||||||
<Map
|
|
||||||
introNodes={mockIntroNodes}
|
|
||||||
nodes={mockChallengeNodes}
|
|
||||||
resetExpansion={() => {}}
|
|
||||||
toggleBlock={() => {}}
|
|
||||||
toggleSuperBlock={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot('Map');
|
expect(container).toMatchSnapshot('Map');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('<Map/>', () => {
|
const props = {
|
||||||
describe('after reload', () => {
|
forLanding: true
|
||||||
const defaultNode = mockChallengeNodes[0];
|
};
|
||||||
const idNode = mockChallengeNodes[7];
|
|
||||||
const hashNode = mockChallengeNodes[9];
|
|
||||||
const currentChallengeId = idNode.id;
|
|
||||||
const hash = dasherize(hashNode.superBlock);
|
|
||||||
|
|
||||||
it('should expand the block with the most recent challenge', () => {
|
|
||||||
const initializeSpy = jest.spyOn(
|
|
||||||
Map.prototype,
|
|
||||||
'initializeExpandedState'
|
|
||||||
);
|
|
||||||
|
|
||||||
const blockSpy = jest.fn();
|
|
||||||
const superSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
toggleBlock: blockSpy,
|
|
||||||
toggleSuperBlock: superSpy
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithRedux(<Map {...props} />);
|
|
||||||
|
|
||||||
expect(blockSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(superSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(initializeSpy).toHaveBeenCalledTimes(1);
|
|
||||||
initializeSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the hash prop if it exists', () => {
|
|
||||||
const blockSpy = jest.fn();
|
|
||||||
const superSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
hash,
|
|
||||||
toggleBlock: blockSpy,
|
|
||||||
toggleSuperBlock: superSpy,
|
|
||||||
currentChallengeId
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithRedux(<Map {...props} />);
|
|
||||||
|
|
||||||
expect(blockSpy).toHaveBeenCalledTimes(1);
|
|
||||||
// the block here should always be the first block of the superblock
|
|
||||||
// this is tested implicitly, as there is a second block in the mock nodes
|
|
||||||
expect(blockSpy).toHaveBeenCalledWith(hashNode.block);
|
|
||||||
|
|
||||||
expect(superSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(superSpy).toHaveBeenCalledWith(hashNode.superBlock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use the currentChallengeId prop if there is no hash', () => {
|
|
||||||
const blockSpy = jest.fn();
|
|
||||||
const superSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
toggleBlock: blockSpy,
|
|
||||||
toggleSuperBlock: superSpy,
|
|
||||||
currentChallengeId
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithRedux(<Map {...props} />);
|
|
||||||
|
|
||||||
expect(blockSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(blockSpy).toHaveBeenCalledWith(idNode.block);
|
|
||||||
|
|
||||||
expect(superSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(superSpy).toHaveBeenCalledWith(idNode.superBlock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should default to the first challenge otherwise', () => {
|
|
||||||
const blockSpy = jest.fn();
|
|
||||||
const superSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
toggleBlock: blockSpy,
|
|
||||||
toggleSuperBlock: superSpy
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithRedux(<Map {...props} />);
|
|
||||||
|
|
||||||
expect(blockSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(blockSpy).toHaveBeenCalledWith(defaultNode.block);
|
|
||||||
|
|
||||||
expect(superSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(superSpy).toHaveBeenCalledWith(defaultNode.superBlock);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls resetExpansion when initializing', () => {
|
|
||||||
const expansionSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
...baseProps,
|
|
||||||
resetExpansion: expansionSpy
|
|
||||||
};
|
|
||||||
|
|
||||||
renderWithRedux(<Map {...props} />);
|
|
||||||
|
|
||||||
expect(expansionSpy).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -3,92 +3,310 @@
|
|||||||
exports[`<Map /> snapshot: Map 1`] = `
|
exports[`<Map /> snapshot: Map 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="map-ui"
|
||||||
|
data-test-label="learn-curriculum-map"
|
||||||
>
|
>
|
||||||
<div
|
<ul
|
||||||
class="col-sm-10 col-sm-offset-1 col-xs-12"
|
data-test-label="certifications"
|
||||||
>
|
>
|
||||||
<div
|
<li>
|
||||||
class="map-ui"
|
<a
|
||||||
data-test-label="learn-curriculum-map"
|
class="btn link-btn btn-lg"
|
||||||
>
|
href="/learn/super-block-one/"
|
||||||
<ul>
|
>
|
||||||
<li
|
Super Block One
|
||||||
class="superblock "
|
<svg
|
||||||
id="super-block-one"
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
<button
|
<g>
|
||||||
aria-expanded="false"
|
<polygon
|
||||||
class="map-title"
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
>
|
/>
|
||||||
<svg
|
<polygon
|
||||||
viewBox="0 0 100 100"
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
width="25px"
|
/>
|
||||||
>
|
</g>
|
||||||
<polygon
|
</svg>
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
</a>
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
</li>
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
<li>
|
||||||
/>
|
<a
|
||||||
</svg>
|
class="btn link-btn btn-lg"
|
||||||
<h4>
|
href="/learn/super-block-one/"
|
||||||
Super Block One Certification (300 hours)
|
>
|
||||||
</h4>
|
Super Block One
|
||||||
</button>
|
<svg
|
||||||
</li>
|
aria-hidden="true"
|
||||||
<li
|
fill="inherit"
|
||||||
class="superblock "
|
height="20px"
|
||||||
id="super-block-two"
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
<button
|
<g>
|
||||||
aria-expanded="false"
|
<polygon
|
||||||
class="map-title"
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
>
|
/>
|
||||||
<svg
|
<polygon
|
||||||
viewBox="0 0 100 100"
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
width="25px"
|
/>
|
||||||
>
|
</g>
|
||||||
<polygon
|
</svg>
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
</a>
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
</li>
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
<li>
|
||||||
/>
|
<a
|
||||||
</svg>
|
class="btn link-btn btn-lg"
|
||||||
<h4>
|
href="/learn/super-block-one/"
|
||||||
Super Block Two Certification (300 hours)
|
>
|
||||||
</h4>
|
Super Block One
|
||||||
</button>
|
<svg
|
||||||
</li>
|
aria-hidden="true"
|
||||||
<li
|
fill="inherit"
|
||||||
class="superblock "
|
height="20px"
|
||||||
id="super-block-three"
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
<button
|
<g>
|
||||||
aria-expanded="false"
|
<polygon
|
||||||
class="map-title"
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
>
|
/>
|
||||||
<svg
|
<polygon
|
||||||
viewBox="0 0 100 100"
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
width="25px"
|
/>
|
||||||
>
|
</g>
|
||||||
<polygon
|
</svg>
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
</a>
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
</li>
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
<li>
|
||||||
/>
|
<a
|
||||||
</svg>
|
class="btn link-btn btn-lg"
|
||||||
<h4>
|
href="/learn/super-block-one/"
|
||||||
Super Block Three Certification (300 hours)
|
>
|
||||||
</h4>
|
Super Block One
|
||||||
</button>
|
<svg
|
||||||
</li>
|
aria-hidden="true"
|
||||||
<div
|
fill="inherit"
|
||||||
class="spacer"
|
height="20px"
|
||||||
style="padding: 15px 0px; height: 1px;"
|
version="1.1"
|
||||||
/>
|
viewBox="0 0 16 20"
|
||||||
</ul>
|
width="18px"
|
||||||
</div>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-one/"
|
||||||
|
>
|
||||||
|
Super Block One
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-two/"
|
||||||
|
>
|
||||||
|
Super Block Two
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-two/"
|
||||||
|
>
|
||||||
|
Super Block Two
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-two/"
|
||||||
|
>
|
||||||
|
Super Block Two
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-two/"
|
||||||
|
>
|
||||||
|
Super Block Two
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-three/"
|
||||||
|
>
|
||||||
|
Super Block Three
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn link-btn btn-lg"
|
||||||
|
href="/learn/super-block-three/"
|
||||||
|
>
|
||||||
|
Super Block Three
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="inherit"
|
||||||
|
height="20px"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
points="-2.68014473e-15 -1.06357708e-13 2.01917516 -1.06357708e-13 8.99824941 9.00746464 2.01917516 18.0149293 -2.66453526e-15 18.0149293 7.00955027 9"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
points="7.99971435 -1.06357708e-13 10.0188895 -1.06357708e-13 16.9979638 9.00746464 10.0188895 18.0149293 7.99971435 18.0149293 15.0092646 9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { uniq, find } from 'lodash';
|
|
||||||
import { dasherize } from '../../../../../utils/slugs';
|
|
||||||
|
|
||||||
import Block from './Block';
|
|
||||||
|
|
||||||
import { makeExpandedSuperBlockSelector, toggleSuperBlock } from '../redux';
|
|
||||||
import Caret from '../../../assets/icons/Caret';
|
|
||||||
import { ChallengeNode } from '../../../redux/propTypes';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
const expandedSelector = makeExpandedSuperBlockSelector(ownProps.superBlock);
|
|
||||||
|
|
||||||
return createSelector(
|
|
||||||
expandedSelector,
|
|
||||||
isExpanded => ({ isExpanded })
|
|
||||||
)(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return bindActionCreators(
|
|
||||||
{
|
|
||||||
toggleSuperBlock
|
|
||||||
},
|
|
||||||
dispatch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
introNodes: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
|
|
||||||
frontmatter: PropTypes.shape({
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
block: PropTypes.string.isRequired
|
|
||||||
})
|
|
||||||
})
|
|
||||||
),
|
|
||||||
isExpanded: PropTypes.bool,
|
|
||||||
nodes: PropTypes.arrayOf(ChallengeNode),
|
|
||||||
superBlock: PropTypes.string,
|
|
||||||
toggleSuperBlock: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const codingPrepRE = new RegExp('Interview Prep');
|
|
||||||
|
|
||||||
function createSuperBlockTitle(str) {
|
|
||||||
return codingPrepRE.test(str)
|
|
||||||
? `${str} (Thousands of hours of challenges)`
|
|
||||||
: `${str} Certification (300\xa0hours)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SuperBlock extends Component {
|
|
||||||
renderBlock(superBlock) {
|
|
||||||
const { nodes, introNodes } = this.props;
|
|
||||||
const blocksForSuperBlock = nodes.filter(
|
|
||||||
node => node.superBlock === superBlock
|
|
||||||
);
|
|
||||||
const blockDashedNames = uniq(
|
|
||||||
blocksForSuperBlock.map(({ block }) => block)
|
|
||||||
);
|
|
||||||
// render all non-empty blocks
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{blockDashedNames.map(blockDashedName => (
|
|
||||||
<Block
|
|
||||||
blockDashedName={blockDashedName}
|
|
||||||
challenges={blocksForSuperBlock.filter(
|
|
||||||
node => node.block === blockDashedName
|
|
||||||
)}
|
|
||||||
intro={find(
|
|
||||||
introNodes,
|
|
||||||
({ frontmatter: { block } }) =>
|
|
||||||
block
|
|
||||||
.toLowerCase()
|
|
||||||
.split(' ')
|
|
||||||
.join('-') === blockDashedName
|
|
||||||
)}
|
|
||||||
key={blockDashedName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { superBlock, isExpanded, toggleSuperBlock } = this.props;
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
className={`superblock ${isExpanded ? 'open' : ''}`}
|
|
||||||
id={dasherize(superBlock)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
className='map-title'
|
|
||||||
onClick={() => toggleSuperBlock(superBlock)}
|
|
||||||
>
|
|
||||||
<Caret />
|
|
||||||
<h4>{createSuperBlockTitle(superBlock)}</h4>
|
|
||||||
</button>
|
|
||||||
{isExpanded ? this.renderBlock(superBlock) : null}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SuperBlock.displayName = 'SuperBlock';
|
|
||||||
SuperBlock.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(SuperBlock);
|
|
@ -1,78 +0,0 @@
|
|||||||
/* global jest, expect */
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { createStore } from '../../../redux/createStore';
|
|
||||||
|
|
||||||
import { SuperBlock } from './SuperBlock';
|
|
||||||
import mockChallengeNodes from '../../../__mocks__/challenge-nodes';
|
|
||||||
import mockIntroNodes from '../../../__mocks__/intro-nodes';
|
|
||||||
|
|
||||||
function renderWithRedux(ui, store) {
|
|
||||||
return render(<Provider store={store || createStore()}>{ui}</Provider>);
|
|
||||||
}
|
|
||||||
|
|
||||||
test('<SuperBlock /> not expanded snapshot', () => {
|
|
||||||
const props = {
|
|
||||||
introNodes: mockIntroNodes,
|
|
||||||
isExpanded: false,
|
|
||||||
nodes: mockChallengeNodes,
|
|
||||||
superBlock: 'Super Block One',
|
|
||||||
toggleSuperBlock: () => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { container } = render(<SuperBlock {...props} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot('superBlock-not-expanded');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('<SuperBlock /> expanded snapshot', () => {
|
|
||||||
const props = {
|
|
||||||
introNodes: mockIntroNodes,
|
|
||||||
isExpanded: true,
|
|
||||||
nodes: mockChallengeNodes,
|
|
||||||
superBlock: 'Super Block One',
|
|
||||||
toggleSuperBlock: () => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { container } = renderWithRedux(<SuperBlock {...props} />);
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot('superBlock-expanded');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('<SuperBlock should handle toggle clicks correctly', () => {
|
|
||||||
const toggleSpy = jest.fn();
|
|
||||||
const props = {
|
|
||||||
introNodes: mockIntroNodes,
|
|
||||||
isExpanded: false,
|
|
||||||
nodes: mockChallengeNodes,
|
|
||||||
superBlock: 'Super Block One',
|
|
||||||
toggleSuperBlock: toggleSpy
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
const { container, rerender } = renderWithRedux(
|
|
||||||
<SuperBlock {...props} />,
|
|
||||||
store
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(toggleSpy).not.toHaveBeenCalled();
|
|
||||||
expect(container.querySelector('.map-title h4')).toHaveTextContent(
|
|
||||||
'Super Block One Certification (300 hours)'
|
|
||||||
);
|
|
||||||
expect(container.querySelector('ul')).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.click(container.querySelector('.map-title'));
|
|
||||||
|
|
||||||
expect(toggleSpy).toHaveBeenCalledTimes(1);
|
|
||||||
expect(toggleSpy).toHaveBeenCalledWith('Super Block One');
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<Provider store={store}>
|
|
||||||
<SuperBlock {...props} isExpanded={true} />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(container.querySelector('ul')).toBeInTheDocument();
|
|
||||||
});
|
|
@ -1,242 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<SuperBlock /> expanded snapshot: superBlock-expanded 1`] = `
|
|
||||||
<div>
|
|
||||||
<li
|
|
||||||
class="superblock open"
|
|
||||||
id="super-block-one"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded="true"
|
|
||||||
class="map-title"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="25px"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h4>
|
|
||||||
Super Block One Certification (300 hours)
|
|
||||||
</h4>
|
|
||||||
</button>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
class="block "
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded="false"
|
|
||||||
class="map-title"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="25px"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h4>
|
|
||||||
Block A
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="map-title-completed"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
class="sr-only"
|
|
||||||
>
|
|
||||||
icons.not-passed
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
height="50"
|
|
||||||
style="height: 15px; margin-right: 10px; width: 15px;"
|
|
||||||
viewBox="0 0 200 200"
|
|
||||||
width="50"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<title>
|
|
||||||
icons.not-passed
|
|
||||||
</title>
|
|
||||||
<circle
|
|
||||||
cx="100"
|
|
||||||
cy="99"
|
|
||||||
fill="var(--primary-background)"
|
|
||||||
r="95"
|
|
||||||
stroke="var(--primary-color)"
|
|
||||||
stroke-dasharray="null"
|
|
||||||
stroke-linecap="null"
|
|
||||||
stroke-linejoin="null"
|
|
||||||
stroke-width="10"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
0/2
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<ul />
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="block "
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded="false"
|
|
||||||
class="map-title"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="25px"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h4>
|
|
||||||
Block B
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="map-title-completed"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
class="sr-only"
|
|
||||||
>
|
|
||||||
icons.not-passed
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
height="50"
|
|
||||||
style="height: 15px; margin-right: 10px; width: 15px;"
|
|
||||||
viewBox="0 0 200 200"
|
|
||||||
width="50"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<title>
|
|
||||||
icons.not-passed
|
|
||||||
</title>
|
|
||||||
<circle
|
|
||||||
cx="100"
|
|
||||||
cy="99"
|
|
||||||
fill="var(--primary-background)"
|
|
||||||
r="95"
|
|
||||||
stroke="var(--primary-color)"
|
|
||||||
stroke-dasharray="null"
|
|
||||||
stroke-linecap="null"
|
|
||||||
stroke-linejoin="null"
|
|
||||||
stroke-width="10"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
0/2
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<ul />
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="block "
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded="false"
|
|
||||||
class="map-title"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="25px"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h4>
|
|
||||||
Block C
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="map-title-completed"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
class="sr-only"
|
|
||||||
>
|
|
||||||
icons.not-passed
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
height="50"
|
|
||||||
style="height: 15px; margin-right: 10px; width: 15px;"
|
|
||||||
viewBox="0 0 200 200"
|
|
||||||
width="50"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<title>
|
|
||||||
icons.not-passed
|
|
||||||
</title>
|
|
||||||
<circle
|
|
||||||
cx="100"
|
|
||||||
cy="99"
|
|
||||||
fill="var(--primary-background)"
|
|
||||||
r="95"
|
|
||||||
stroke="var(--primary-color)"
|
|
||||||
stroke-dasharray="null"
|
|
||||||
stroke-linecap="null"
|
|
||||||
stroke-linejoin="null"
|
|
||||||
stroke-width="10"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
0/1
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<ul />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<SuperBlock /> not expanded snapshot: superBlock-not-expanded 1`] = `
|
|
||||||
<div>
|
|
||||||
<li
|
|
||||||
class="superblock "
|
|
||||||
id="super-block-one"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-expanded="false"
|
|
||||||
class="map-title"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
width="25px"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="-6.04047,17.1511 81.8903,58.1985 -3.90024,104.196"
|
|
||||||
style="stroke: var(--primary-color); fill: var(--primary-color); stroke-width: 1px;"
|
|
||||||
transform="matrix(0.999729, 0.023281, -0.023281, 0.999729, 7.39321, -10.0425)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h4>
|
|
||||||
Super Block One Certification (300 hours)
|
|
||||||
</h4>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
`;
|
|
@ -1,138 +1,98 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import uniq from 'lodash/uniq';
|
import { graphql, useStaticQuery } from 'gatsby';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import SuperBlock from './components/SuperBlock';
|
import { Link } from '../helpers';
|
||||||
import Spacer from '../helpers/Spacer';
|
import LinkButton from '../../assets/icons/LinkButton';
|
||||||
|
|
||||||
import './map.css';
|
|
||||||
import { ChallengeNode } from '../../redux/propTypes';
|
|
||||||
import { toggleSuperBlock, toggleBlock, resetExpansion } from './redux';
|
|
||||||
import { currentChallengeIdSelector } from '../../redux';
|
|
||||||
import { dasherize } from '../../../../utils/slugs';
|
import { dasherize } from '../../../../utils/slugs';
|
||||||
|
import './map.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
currentChallengeId: PropTypes.string,
|
forLanding: PropTypes.bool
|
||||||
hash: PropTypes.string,
|
|
||||||
introNodes: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
fields: PropTypes.shape({ slug: PropTypes.string.isRequired }),
|
|
||||||
frontmatter: PropTypes.shape({
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
block: PropTypes.string.isRequired
|
|
||||||
})
|
|
||||||
})
|
|
||||||
),
|
|
||||||
isSignedIn: PropTypes.bool,
|
|
||||||
nodes: PropTypes.arrayOf(ChallengeNode),
|
|
||||||
resetExpansion: PropTypes.func,
|
|
||||||
toggleBlock: PropTypes.func.isRequired,
|
|
||||||
toggleSuperBlock: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const codingPrepRE = new RegExp('Interview Prep');
|
||||||
return createSelector(
|
|
||||||
currentChallengeIdSelector,
|
|
||||||
currentChallengeId => ({
|
|
||||||
currentChallengeId
|
|
||||||
})
|
|
||||||
)(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function createSuperBlockTitle(str) {
|
||||||
return bindActionCreators(
|
return codingPrepRE.test(str)
|
||||||
{
|
? `${str} (Thousands of hours of challenges)`
|
||||||
resetExpansion,
|
: `${str} Certification (300\xa0hours)`;
|
||||||
toggleSuperBlock,
|
}
|
||||||
toggleBlock
|
|
||||||
},
|
function renderLandingMap(nodes) {
|
||||||
dispatch
|
return (
|
||||||
|
<ul data-test-label='certifications'>
|
||||||
|
{nodes.map((node, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<Link
|
||||||
|
className='btn link-btn btn-lg'
|
||||||
|
to={`/learn/${dasherize(node.superBlock)}/`}
|
||||||
|
>
|
||||||
|
{node.superBlock}
|
||||||
|
<LinkButton />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Map extends Component {
|
function renderLearnMap(nodes) {
|
||||||
constructor(props) {
|
return (
|
||||||
super(props);
|
<Row>
|
||||||
this.initializeExpandedState();
|
<Col sm={10} smOffset={1} xs={12}>
|
||||||
}
|
<ul data-test-label='learn-curriculum-map'>
|
||||||
|
{nodes.map((node, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<Link
|
||||||
|
className='btn link-btn btn-lg'
|
||||||
|
to={`/learn/${dasherize(node.superBlock)}/`}
|
||||||
|
>
|
||||||
|
{createSuperBlockTitle(node.superBlock)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// As this happens in the constructor, it's necessary to manipulate state
|
export function Map({ forLanding = false }) {
|
||||||
// directly.
|
/*
|
||||||
initializeExpandedState() {
|
* this query gets the first challenge from each block and the second block
|
||||||
const {
|
* from each superblock, leaving you with one challenge from each
|
||||||
currentChallengeId,
|
* superblock
|
||||||
hash,
|
*/
|
||||||
nodes,
|
const data = useStaticQuery(graphql`
|
||||||
resetExpansion,
|
query SuperBlockNodes {
|
||||||
toggleBlock,
|
allChallengeNode(
|
||||||
toggleSuperBlock,
|
sort: { fields: [superOrder] }
|
||||||
isSignedIn
|
filter: { order: { eq: 2 }, challengeOrder: { eq: 1 } }
|
||||||
} = this.props;
|
) {
|
||||||
resetExpansion();
|
nodes {
|
||||||
|
superBlock
|
||||||
let node;
|
dashedName
|
||||||
|
}
|
||||||
// find the challenge that has the same superblock with hash
|
|
||||||
if (hash) {
|
|
||||||
node = nodes.find(node => dasherize(node.superBlock) === hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
// without hash only expand when signed in
|
|
||||||
if (isSignedIn) {
|
|
||||||
// if there is no hash or the hash did not match any challenge superblock
|
|
||||||
// and there was a currentChallengeId
|
|
||||||
if (!node && currentChallengeId) {
|
|
||||||
node = nodes.find(node => node.id === currentChallengeId);
|
|
||||||
}
|
}
|
||||||
if (!node) node = nodes[0];
|
|
||||||
}
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
if (!node) return;
|
let nodes = data.allChallengeNode.nodes;
|
||||||
|
|
||||||
toggleBlock(node.block);
|
if (forLanding) {
|
||||||
toggleSuperBlock(node.superBlock);
|
nodes = nodes.filter(node => node.superBlock !== 'Coding Interview Prep');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuperBlocks(superBlocks) {
|
return (
|
||||||
const { nodes, introNodes } = this.props;
|
<div className='map-ui' data-test-label='learn-curriculum-map'>
|
||||||
return superBlocks.map(superBlock => (
|
{forLanding ? renderLandingMap(nodes) : renderLearnMap(nodes)}
|
||||||
<SuperBlock
|
</div>
|
||||||
introNodes={introNodes}
|
);
|
||||||
key={superBlock}
|
|
||||||
nodes={nodes}
|
|
||||||
superBlock={superBlock}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { nodes } = this.props;
|
|
||||||
// if a given superBlock's nodes have been filtered that
|
|
||||||
// superBlock will not appear in superBlocks and will not be rendered.
|
|
||||||
const superBlocks = uniq(nodes.map(({ superBlock }) => superBlock));
|
|
||||||
return (
|
|
||||||
<Row>
|
|
||||||
<Col sm={10} smOffset={1} xs={12}>
|
|
||||||
<div className='map-ui' data-test-label='learn-curriculum-map'>
|
|
||||||
<ul>
|
|
||||||
{this.renderSuperBlocks(superBlocks)}
|
|
||||||
<Spacer />
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map.displayName = 'Map';
|
Map.displayName = 'Map';
|
||||||
Map.propTypes = propTypes;
|
Map.propTypes = propTypes;
|
||||||
|
|
||||||
export default connect(
|
export default Map;
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Map);
|
|
||||||
|
@ -28,6 +28,24 @@ button.map-title:hover {
|
|||||||
background-color: var(--tertiary-background);
|
background-color: var(--tertiary-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.map-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-top: 18px;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
padding-left: 5px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.map-title:hover {
|
||||||
|
color: var(--tertiary-color);
|
||||||
|
background-color: var(--tertiary-background);
|
||||||
|
}
|
||||||
|
|
||||||
.map-title > h4 {
|
.map-title > h4 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@ -38,7 +56,7 @@ button.map-title:hover {
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.superblock .map-title svg {
|
.map-title svg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -86,7 +104,14 @@ li.open > .map-title svg:first-child {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 14px is the width of the expansion arrow */
|
.block {
|
||||||
.superblock {
|
|
||||||
max-width: calc(100% - 14px);
|
max-width: calc(100% - 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block svg {
|
||||||
|
width: 14px;
|
||||||
|
margin-right: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
fill: var(--color-quaternary) !important;
|
||||||
|
stroke: var(--color-quaternary);
|
||||||
|
}
|
||||||
|
@ -1,29 +1,19 @@
|
|||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import { createTypes } from '../../../../utils/stateManagement';
|
import { createTypes } from '../../../../utils/stateManagement';
|
||||||
|
|
||||||
export const ns = 'curriculumMap';
|
export const ns = 'curriculumMap';
|
||||||
|
|
||||||
export const getNS = () => ns;
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
expandedState: {
|
expandedState: {
|
||||||
superBlock: {},
|
|
||||||
block: {}
|
block: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const types = createTypes(
|
const types = createTypes(['resetExpansion', 'toggleBlock'], ns);
|
||||||
['resetExpansion', 'toggleSuperBlock', 'toggleBlock'],
|
|
||||||
ns
|
|
||||||
);
|
|
||||||
|
|
||||||
export const resetExpansion = createAction(types.resetExpansion);
|
export const resetExpansion = createAction(types.resetExpansion);
|
||||||
export const toggleBlock = createAction(types.toggleBlock);
|
export const toggleBlock = createAction(types.toggleBlock);
|
||||||
export const toggleSuperBlock = createAction(types.toggleSuperBlock);
|
|
||||||
|
|
||||||
export const makeExpandedSuperBlockSelector = superBlock => state =>
|
|
||||||
!!state[ns].expandedState.superBlock[superBlock];
|
|
||||||
export const makeExpandedBlockSelector = block => state =>
|
export const makeExpandedBlockSelector = block => state =>
|
||||||
!!state[ns].expandedState.block[block];
|
!!state[ns].expandedState.block[block];
|
||||||
|
|
||||||
@ -32,7 +22,6 @@ export const reducer = handleActions(
|
|||||||
[types.resetExpansion]: state => ({
|
[types.resetExpansion]: state => ({
|
||||||
...state,
|
...state,
|
||||||
expandedState: {
|
expandedState: {
|
||||||
superBlock: {},
|
|
||||||
block: {}
|
block: {}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -45,16 +34,6 @@ export const reducer = handleActions(
|
|||||||
[payload]: !state.expandedState.block[payload]
|
[payload]: !state.expandedState.block[payload]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
[types.toggleSuperBlock]: (state, { payload }) => ({
|
|
||||||
...state,
|
|
||||||
expandedState: {
|
|
||||||
...state.expandedState,
|
|
||||||
superBlock: {
|
|
||||||
...state.expandedState.superBlock,
|
|
||||||
[payload]: !state.expandedState.superBlock[payload]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
|
@ -1,41 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
import { Col, Row } from '@freecodecamp/react-bootstrap';
|
||||||
import { uniq } from 'lodash';
|
import { Spacer } from '../../helpers';
|
||||||
import { Spacer, Link } from '../../helpers';
|
|
||||||
import LinkButton from '../../../assets/icons/LinkButton';
|
|
||||||
import BigCallToAction from './BigCallToAction';
|
import BigCallToAction from './BigCallToAction';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Map from '../../Map/index';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
nodes: PropTypes.array,
|
|
||||||
page: PropTypes.string
|
page: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
const Certifications = ({ nodes, page }) => {
|
const Certifications = ({ page = 'landing' }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const superBlocks = uniq(nodes.map(node => node.superBlock)).filter(
|
|
||||||
cert => cert !== 'Coding Interview Prep'
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className='certification-section'>
|
<Row className='certification-section'>
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
<p className='big-heading'>{t('landing.certification-heading')}</p>
|
<h1 className='big-heading'>{t('landing.certification-heading')}</h1>
|
||||||
<ul data-test-label='certifications'>
|
<Map forLanding={true} />
|
||||||
{superBlocks.map((superBlock, i) => (
|
|
||||||
<li key={i}>
|
|
||||||
<Link
|
|
||||||
className='btn link-btn btn-lg'
|
|
||||||
state={{ superBlock: superBlock }}
|
|
||||||
to={`/learn`}
|
|
||||||
>
|
|
||||||
{superBlock}
|
|
||||||
<LinkButton />
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<BigCallToAction page={page} />
|
<BigCallToAction page={page} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
@ -2,7 +2,6 @@ import React, { Fragment } from 'react';
|
|||||||
import { Grid } from '@freecodecamp/react-bootstrap';
|
import { Grid } from '@freecodecamp/react-bootstrap';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { graphql, useStaticQuery } from 'gatsby';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Testimonials from './components/Testimonials';
|
import Testimonials from './components/Testimonials';
|
||||||
@ -19,18 +18,6 @@ const propTypes = {
|
|||||||
export const Landing = ({ page = 'landing' }) => {
|
export const Landing = ({ page = 'landing' }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const data = useStaticQuery(graphql`
|
|
||||||
query certifications {
|
|
||||||
challenges: allChallengeNode(
|
|
||||||
sort: { fields: [superOrder, order, challengeOrder] }
|
|
||||||
) {
|
|
||||||
nodes {
|
|
||||||
superBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -45,7 +32,7 @@ export const Landing = ({ page = 'landing' }) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
<Certifications nodes={data.challenges.nodes} page={page} />
|
<Certifications />
|
||||||
</Grid>
|
</Grid>
|
||||||
</main>
|
</main>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment, Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -142,7 +142,7 @@ class DefaultLayout extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<div className='page-wrapper'>
|
||||||
<Helmet
|
<Helmet
|
||||||
bodyAttributes={{
|
bodyAttributes={{
|
||||||
class: useTheme
|
class: useTheme
|
||||||
@ -202,17 +202,17 @@ class DefaultLayout extends Component {
|
|||||||
<style>{fontawesome.dom.css()}</style>
|
<style>{fontawesome.dom.css()}</style>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<WithInstantSearch>
|
<WithInstantSearch>
|
||||||
<Header fetchState={fetchState} user={user} />
|
|
||||||
<div className={`default-layout`}>
|
<div className={`default-layout`}>
|
||||||
|
<Header fetchState={fetchState} user={user} />
|
||||||
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
||||||
{hasMessage && flashMessage ? (
|
{hasMessage && flashMessage ? (
|
||||||
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
|
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
{showFooter && <Footer />}
|
|
||||||
</div>
|
</div>
|
||||||
|
{showFooter && <Footer />}
|
||||||
</WithInstantSearch>
|
</WithInstantSearch>
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,30 @@
|
|||||||
html {
|
html {
|
||||||
|
height: 100%;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
height: 100%;
|
||||||
font-family: 'Roboto Mono', monospace;
|
font-family: 'Roboto Mono', monospace;
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
background: var(--secondary-background);
|
background: var(--secondary-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#___gatsby {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gatsby-focus-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-cta-big {
|
.btn-cta-big {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@ -16,12 +32,20 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Row from @freecodecamp/react-bootstrap adds some negative margin which causes
|
||||||
|
a little bit of horizontal overflow on certain pages. This eliminates that
|
||||||
|
for those pages.
|
||||||
|
*/
|
||||||
|
.overflow-fix {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.default-layout {
|
.default-layout {
|
||||||
|
flex: 1 0 auto;
|
||||||
margin-top: var(--header-height, 0px);
|
margin-top: var(--header-height, 0px);
|
||||||
background: var(--secondary-background);
|
background: var(--secondary-background);
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: calc(100vh - var(--header-height, 0px));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -7,8 +7,8 @@ import Helmet from 'react-helmet';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Spacer } from '../components/helpers';
|
||||||
import LearnLayout from '../components/layouts/Learn';
|
import LearnLayout from '../components/layouts/Learn';
|
||||||
import { dasherize } from '../../../utils/slugs';
|
|
||||||
import Map from '../components/Map';
|
import Map from '../components/Map';
|
||||||
import Intro from '../components/Intro';
|
import Intro from '../components/Intro';
|
||||||
import {
|
import {
|
||||||
@ -16,11 +16,7 @@ import {
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userSelector
|
userSelector
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
import {
|
import { ChallengeNode } from '../redux/propTypes';
|
||||||
ChallengeNode,
|
|
||||||
AllChallengeNode,
|
|
||||||
AllMarkdownRemark
|
|
||||||
} from '../redux/propTypes';
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
@ -35,18 +31,14 @@ const mapStateToProps = createSelector(
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
challengeNode: ChallengeNode,
|
challengeNode: ChallengeNode
|
||||||
allChallengeNode: AllChallengeNode,
|
|
||||||
allMarkdownRemark: AllMarkdownRemark
|
|
||||||
}),
|
}),
|
||||||
fetchState: PropTypes.shape({
|
fetchState: PropTypes.shape({
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
complete: PropTypes.bool,
|
complete: PropTypes.bool,
|
||||||
errored: PropTypes.bool
|
errored: PropTypes.bool
|
||||||
}),
|
}),
|
||||||
hash: PropTypes.string,
|
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
location: PropTypes.object,
|
|
||||||
state: PropTypes.object,
|
state: PropTypes.object,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
@ -55,28 +47,18 @@ const propTypes = {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// choose between the state from landing page and hash from url.
|
|
||||||
const hashValueSelector = (state, hash) => {
|
|
||||||
if (state && state.superBlock) return dasherize(state.superBlock);
|
|
||||||
else if (hash) return hash.substr(1);
|
|
||||||
else return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LearnPage = ({
|
export const LearnPage = ({
|
||||||
location: { hash = '', state = '' },
|
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
fetchState: { pending, complete },
|
fetchState: { pending, complete },
|
||||||
user: { name = '', completedChallengeCount = 0 },
|
user: { name = '', completedChallengeCount = 0 },
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
fields: { slug }
|
fields: { slug }
|
||||||
},
|
}
|
||||||
allChallengeNode: { edges },
|
|
||||||
allMarkdownRemark: { edges: mdEdges }
|
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const hashValue = hashValueSelector(state, hash);
|
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<LearnLayout>
|
||||||
<Helmet title={t('meta.title')} />
|
<Helmet title={t('meta.title')} />
|
||||||
@ -89,14 +71,8 @@ export const LearnPage = ({
|
|||||||
pending={pending}
|
pending={pending}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
/>
|
/>
|
||||||
<Map
|
<Map />
|
||||||
hash={hashValue}
|
<Spacer size={2} />
|
||||||
introNodes={mdEdges.map(({ node }) => node)}
|
|
||||||
isSignedIn={isSignedIn}
|
|
||||||
nodes={edges
|
|
||||||
.map(({ node }) => node)
|
|
||||||
.filter(({ isPrivate }) => !isPrivate)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</LearnLayout>
|
</LearnLayout>
|
||||||
);
|
);
|
||||||
@ -114,33 +90,5 @@ export const query = graphql`
|
|||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
fields {
|
|
||||||
slug
|
|
||||||
blockName
|
|
||||||
}
|
|
||||||
id
|
|
||||||
block
|
|
||||||
title
|
|
||||||
superBlock
|
|
||||||
dashedName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allMarkdownRemark(filter: { frontmatter: { block: { ne: null } } }) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
frontmatter {
|
|
||||||
title
|
|
||||||
block
|
|
||||||
}
|
|
||||||
fields {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
10
client/src/pages/learn/coding-interview-prep/index.md
Normal file
10
client/src/pages/learn/coding-interview-prep/index.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Coding Interview Prep
|
||||||
|
superBlock: Coding Interview Prep
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introduction to Coding Interview Prep
|
||||||
|
|
||||||
|
This introduction is a stub
|
||||||
|
|
||||||
|
Help us make it real on [GitHub](https://github.com/freeCodeCamp/learn/tree/master/src/introductions).
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Introduction to the Python for Penetration Testing Lectures
|
title: Introduction to the Python for Penetration Testing Lectures
|
||||||
block: Python for Penetration Testing
|
block: Python for Penetration Testing
|
||||||
superBlock: Data Analysis with Python
|
superBlock: Information Security
|
||||||
---
|
---
|
||||||
## Introduction to the Python for Penetration Testing Challenges
|
## Introduction to the Python for Penetration Testing Challenges
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Introduction to the How Neural Networks Work Lectures
|
title: Introduction to the How Neural Networks Work Lectures
|
||||||
block: How Neural Networks Work
|
block: How Neural Networks Work
|
||||||
superBlock: Data Analysis with Python
|
superBlock: Machine Learning with Python
|
||||||
---
|
---
|
||||||
## Introduction to the How Neural Networks Work Challenges
|
## Introduction to the How Neural Networks Work Challenges
|
||||||
|
|
||||||
|
@ -1,49 +1,206 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
|
import { uniq, find } from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import FullWidthRow from '../../components/helpers/FullWidthRow';
|
import Block from '../../components/Map/components/Block';
|
||||||
import { MarkdownRemark } from '../../redux/propTypes';
|
import { FullWidthRow, Spacer } from '../../components/helpers';
|
||||||
|
import { currentChallengeIdSelector, isSignedInSelector } from '../../redux';
|
||||||
|
import { resetExpansion, toggleBlock } from '../../components/Map/redux';
|
||||||
|
import {
|
||||||
|
MarkdownRemark,
|
||||||
|
AllChallengeNode,
|
||||||
|
AllMarkdownRemark
|
||||||
|
} from '../../redux/propTypes';
|
||||||
|
|
||||||
|
import './intro.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
currentChallengeId: PropTypes.string,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
markdownRemark: MarkdownRemark
|
markdownRemark: MarkdownRemark,
|
||||||
})
|
allChallengeNode: AllChallengeNode,
|
||||||
|
allMarkdownRemark: AllMarkdownRemark
|
||||||
|
}),
|
||||||
|
expandedState: PropTypes.object,
|
||||||
|
isSignedIn: PropTypes.bool,
|
||||||
|
resetExpansion: PropTypes.func,
|
||||||
|
toggleBlock: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
function SuperBlockIntroductionPage({ data: { markdownRemark } }) {
|
const mapStateToProps = state => {
|
||||||
const {
|
return createSelector(
|
||||||
html,
|
currentChallengeIdSelector,
|
||||||
frontmatter: { superBlock }
|
isSignedInSelector,
|
||||||
} = markdownRemark;
|
(currentChallengeId, isSignedIn) => ({
|
||||||
return (
|
currentChallengeId,
|
||||||
<Fragment>
|
isSignedIn
|
||||||
<Helmet>
|
})
|
||||||
<title>{superBlock} | freeCodeCamp.org</title>
|
)(state);
|
||||||
</Helmet>
|
};
|
||||||
<FullWidthRow>
|
|
||||||
<div
|
const mapDispatchToProps = dispatch =>
|
||||||
className='intro-layout'
|
bindActionCreators(
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
{ resetExpansion, toggleBlock: b => toggleBlock(b) },
|
||||||
/>
|
dispatch
|
||||||
</FullWidthRow>
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function renderBlock(blocksForSuperBlock, introNodes) {
|
||||||
|
// since the nodes have been filtered based on isHidden, any blocks whose
|
||||||
|
// nodes have been entirely removed will not appear in this array.
|
||||||
|
const blockDashedNames = uniq(blocksForSuperBlock.map(({ block }) => block));
|
||||||
|
|
||||||
|
// render all non-empty blocks
|
||||||
|
return (
|
||||||
|
<ul className='block'>
|
||||||
|
{blockDashedNames.map(blockDashedName => (
|
||||||
|
<Block
|
||||||
|
blockDashedName={blockDashedName}
|
||||||
|
challenges={blocksForSuperBlock.filter(
|
||||||
|
node => node.block === blockDashedName
|
||||||
|
)}
|
||||||
|
intro={find(
|
||||||
|
introNodes,
|
||||||
|
({ frontmatter: { block } }) =>
|
||||||
|
block
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.join('-') === blockDashedName
|
||||||
|
)}
|
||||||
|
key={blockDashedName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SuperBlockIntroductionPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.initializeExpandedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeExpandedState() {
|
||||||
|
const {
|
||||||
|
resetExpansion,
|
||||||
|
data: {
|
||||||
|
allChallengeNode: { edges }
|
||||||
|
},
|
||||||
|
isSignedIn,
|
||||||
|
currentChallengeId,
|
||||||
|
toggleBlock
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
resetExpansion();
|
||||||
|
|
||||||
|
let edge;
|
||||||
|
|
||||||
|
if (isSignedIn) {
|
||||||
|
// see if currentChallenge is in this superBlock
|
||||||
|
edge = edges.find(edge => edge.node.id === currentChallengeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// else, find first block in superBlock
|
||||||
|
let i = 0;
|
||||||
|
while (!edge && i < 20) {
|
||||||
|
// eslint-disable-next-line no-loop-func
|
||||||
|
edge = edges.find(edge => edge.node.order === i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge) toggleBlock(edge.node.block);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
markdownRemark: {
|
||||||
|
frontmatter: { superBlock }
|
||||||
|
},
|
||||||
|
allChallengeNode: { edges },
|
||||||
|
allMarkdownRemark: { edges: mdEdges }
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Helmet>
|
||||||
|
<title>{superBlock} | freeCodeCamp.org</title>
|
||||||
|
</Helmet>
|
||||||
|
<FullWidthRow className='overflow-fix'>
|
||||||
|
<Spacer size={2} />
|
||||||
|
<h1 className='text-center'>
|
||||||
|
{superBlock}
|
||||||
|
{superBlock !== 'Coding Interview Prep' ? ' Certification' : ''}
|
||||||
|
</h1>
|
||||||
|
<Spacer />
|
||||||
|
<div className='block-ui'>
|
||||||
|
{renderBlock(
|
||||||
|
edges.map(({ node }) => node),
|
||||||
|
mdEdges.map(({ node }) => node)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Spacer />
|
||||||
|
</FullWidthRow>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage';
|
SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage';
|
||||||
SuperBlockIntroductionPage.propTypes = propTypes;
|
SuperBlockIntroductionPage.propTypes = propTypes;
|
||||||
|
|
||||||
export default SuperBlockIntroductionPage;
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SuperBlockIntroductionPage);
|
||||||
|
|
||||||
export const query = graphql`
|
export const query = graphql`
|
||||||
query SuperBlockIntroPageBySlug($slug: String!) {
|
query SuperBlockIntroPageBySlug($slug: String!, $superBlock: String!) {
|
||||||
markdownRemark(fields: { slug: { eq: $slug } }) {
|
markdownRemark(fields: { slug: { eq: $slug } }) {
|
||||||
frontmatter {
|
frontmatter {
|
||||||
superBlock
|
superBlock
|
||||||
}
|
}
|
||||||
html
|
}
|
||||||
|
allChallengeNode(
|
||||||
|
sort: { fields: [superOrder, order, challengeOrder] }
|
||||||
|
filter: { superBlock: { eq: $superBlock } }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
blockName
|
||||||
|
}
|
||||||
|
id
|
||||||
|
block
|
||||||
|
title
|
||||||
|
order
|
||||||
|
superBlock
|
||||||
|
dashedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allMarkdownRemark(
|
||||||
|
filter: {
|
||||||
|
frontmatter: { block: { ne: null }, superBlock: { eq: $superBlock } }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
frontmatter {
|
||||||
|
title
|
||||||
|
block
|
||||||
|
}
|
||||||
|
fields {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1,3 +1,20 @@
|
|||||||
|
.block-ui {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-ui ul {
|
||||||
|
list-style: none;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-ui > ul {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
.intro-layout-container {
|
.intro-layout-container {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
@ -122,7 +122,7 @@ exports.createSuperBlockIntroPages = createPage => edge => {
|
|||||||
path: slug,
|
path: slug,
|
||||||
component: superBlockIntro,
|
component: superBlockIntro,
|
||||||
context: {
|
context: {
|
||||||
superBlock: dasherize(superBlock),
|
superBlock: superBlock,
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ export default function layoutSelector({ element, props }) {
|
|||||||
const {
|
const {
|
||||||
location: { pathname }
|
location: { pathname }
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (element.type === FourOhFourPage) {
|
if (element.type === FourOhFourPage) {
|
||||||
return <DefaultLayout pathname={pathname}>{element}</DefaultLayout>;
|
return <DefaultLayout pathname={pathname}>{element}</DefaultLayout>;
|
||||||
}
|
}
|
||||||
@ -22,10 +23,14 @@ export default function layoutSelector({ element, props }) {
|
|||||||
if (/^\/guide(\/.*)*/.test(pathname)) {
|
if (/^\/guide(\/.*)*/.test(pathname)) {
|
||||||
console.log('Hitting guide for some reason. Need a redirect.');
|
console.log('Hitting guide for some reason. Need a redirect.');
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
/^\/learn(\/.*)*/.test(pathname) &&
|
const splitPath = pathname.split('/');
|
||||||
false === /^\/learn\/$|^\/learn$/.test(pathname)
|
const splitPathThree = splitPath.length > 2 ? splitPath[3] : '';
|
||||||
) {
|
|
||||||
|
const isNotSuperBlockIntro =
|
||||||
|
splitPath.length > 3 && splitPathThree.length > 1;
|
||||||
|
|
||||||
|
if (/^\/learn(\/.*)*/.test(pathname) && isNotSuperBlockIntro) {
|
||||||
return (
|
return (
|
||||||
<DefaultLayout pathname={pathname} showFooter={false}>
|
<DefaultLayout pathname={pathname} showFooter={false}>
|
||||||
{element}
|
{element}
|
||||||
|
@ -41,7 +41,7 @@ describe('Learn Landing page (not logged in)', () => {
|
|||||||
it('Should render a curriculum map', () => {
|
it('Should render a curriculum map', () => {
|
||||||
cy.document().then(document => {
|
cy.document().then(document => {
|
||||||
const superBlocks = document.querySelectorAll(
|
const superBlocks = document.querySelectorAll(
|
||||||
`${selectors.challengeMap} > ul > li`
|
`${selectors.challengeMap} > li > a`
|
||||||
);
|
);
|
||||||
expect(superBlocks).to.have.length(11);
|
expect(superBlocks).to.have.length(11);
|
||||||
|
|
||||||
@ -77,44 +77,9 @@ describe('Superblocks and Blocks', () => {
|
|||||||
cy.contains("Get started (it's free)").click();
|
cy.contains("Get started (it's free)").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Has first superblock and block collapsed by default', () => {
|
it('Has all supeblocks visible', () => {
|
||||||
cy.contains(superBlockNames[0])
|
|
||||||
.should('be.visible')
|
|
||||||
.and('have.attr', 'aria-expanded', 'true');
|
|
||||||
|
|
||||||
cy.contains('Basic HTML and HTML5')
|
|
||||||
.should('be.visible')
|
|
||||||
.and('have.attr', 'aria-expanded', 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Has all supeblocks visible but folded (excluding the first one)', () => {
|
|
||||||
cy.wrap(superBlockNames.slice(1)).each(name => {
|
cy.wrap(superBlockNames.slice(1)).each(name => {
|
||||||
cy.contains(name)
|
cy.contains(name).should('be.visible');
|
||||||
.should('be.visible')
|
|
||||||
.and('have.attr', 'aria-expanded', 'false');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('Superblocks should be collapsable and foldable', () => {
|
|
||||||
cy.contains(superBlockNames[0])
|
|
||||||
.click()
|
|
||||||
.should('have.attr', 'aria-expanded', 'false');
|
|
||||||
cy.contains('Basic HTML and HTML5').should('not.exist');
|
|
||||||
|
|
||||||
cy.contains(superBlockNames[0])
|
|
||||||
.click()
|
|
||||||
.should('have.attr', 'aria-expanded', 'true');
|
|
||||||
cy.contains('Basic HTML and HTML5').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Blocks should be collapsable and foldable', () => {
|
|
||||||
cy.contains('Basic HTML and HTML5')
|
|
||||||
.click()
|
|
||||||
.should('have.attr', 'aria-expanded', 'false');
|
|
||||||
cy.contains('Introduction to Basic HTML and HTML5').should('not.exist');
|
|
||||||
|
|
||||||
cy.contains('Basic HTML and HTML5')
|
|
||||||
.click()
|
|
||||||
.should('have.attr', 'aria-expanded', 'true');
|
|
||||||
cy.contains('Introduction to Basic HTML and HTML5').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -9480,6 +9480,12 @@
|
|||||||
"detect-newline": "^2.1.0"
|
"detect-newline": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jest-dom": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-dom/-/jest-dom-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-gBxYZlZB1Jgvf2gP2pRfjjUWF8woGBHj/g5rAQgFPB/0K2atGuhVcPO+BItyjWeKg9zM+dokgcMOH01vrWVMFA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"jest-each": {
|
"jest-each": {
|
||||||
"version": "24.9.0",
|
"version": "24.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz",
|
||||||
|
@ -77,7 +77,8 @@
|
|||||||
"gray-matter": "^4.0.2",
|
"gray-matter": "^4.0.2",
|
||||||
"husky": "^3.1.0",
|
"husky": "^3.1.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
"js-yaml": "^3.14.1",
|
"jest-dom": "^4.0.0",
|
||||||
|
"js-yaml": "^3.14.0",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
"lint-staged": "^8.2.1",
|
"lint-staged": "^8.2.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
Reference in New Issue
Block a user