fix(learn): split and simplified learn map (#39154)

This commit is contained in:
Kabindra Shrestha
2020-12-28 20:52:41 -06:00
committed by Mrugesh Mohapatra
parent 48c97238fc
commit ac3d762bb5
25 changed files with 694 additions and 963 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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