fix(client): merge and update the learn map (#36822)

This commit is contained in:
mrugesh
2019-09-21 19:09:48 +05:30
committed by Ahmad Abdolsaheb
parent a5b176be88
commit e54a7fb350
16 changed files with 561 additions and 581 deletions

View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -30,8 +30,8 @@ describe('<NavLinks />', () => {
it('renders to the DOM', () => { it('renders to the DOM', () => {
expect(root).toBeTruthy(); expect(root).toBeTruthy();
}); });
it('has 2 a tags', () => { it('has 3 a tags', () => {
expect(aTags.length === 2).toBeTruthy(); expect(aTags.length === 3).toBeTruthy();
}); });
it('has link to portfolio', () => { it('has link to portfolio', () => {

View File

@ -26,7 +26,7 @@ const createOnClick = (navigate, isSignedIn) => e => {
e.preventDefault(); e.preventDefault();
gtagReportConversion(); gtagReportConversion();
if (isSignedIn) { if (isSignedIn) {
return gatsbyNavigate('/'); return gatsbyNavigate('/learn');
} }
return navigate(`${apiLocation}/signin`); return navigate(`${apiLocation}/signin`);
}; };

View File

@ -5,6 +5,9 @@ export function NavLinks() {
return ( return (
<div className='main-nav-group'> <div className='main-nav-group'>
<ul className={'nav-list display-flex'} role='menu'> <ul className={'nav-list display-flex'} role='menu'>
<li className='nav-theme' role='menuitem'>
<Link to='/learn'>Projects</Link>
</li>
<li className='nav-theme' role='menuitem'> <li className='nav-theme' role='menuitem'>
<Link to='/'>Light</Link> <Link to='/'>Light</Link>
</li> </li>

View File

@ -1,99 +0,0 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import uniq from 'lodash/uniq';
import { createSelector } from 'reselect';
import SuperBlock from './components/SuperBlock';
import Spacer from '../helpers/Spacer';
import './map.css';
import { ChallengeNode } from '../../redux/propTypes';
import { toggleSuperBlock, toggleBlock, resetExpansion } from './redux';
import { currentChallengeIdSelector } from '../../redux';
const propTypes = {
currentChallengeId: 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
})
})
),
nodes: PropTypes.arrayOf(ChallengeNode),
resetExpansion: PropTypes.func,
toggleBlock: PropTypes.func.isRequired,
toggleSuperBlock: PropTypes.func.isRequired
};
const mapStateToProps = state => {
return createSelector(
currentChallengeIdSelector,
currentChallengeId => ({
currentChallengeId
})
)(state);
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
resetExpansion,
toggleSuperBlock,
toggleBlock
},
dispatch
);
}
export class Map extends Component {
componentDidMount() {
this.initializeExpandedState(this.props.currentChallengeId);
}
initializeExpandedState(currentChallengeId) {
this.props.resetExpansion();
const { superBlock, block } = currentChallengeId
? this.props.nodes.find(node => node.id === currentChallengeId)
: this.props.nodes[0];
this.props.toggleBlock(block);
this.props.toggleSuperBlock(superBlock);
}
renderSuperBlocks(superBlocks) {
const { nodes, introNodes } = this.props;
return superBlocks.map(superBlock => (
<SuperBlock
introNodes={introNodes}
key={superBlock}
nodes={nodes}
superBlock={superBlock}
/>
));
}
render() {
const { nodes } = this.props;
const superBlocks = uniq(nodes.map(({ superBlock }) => superBlock));
return (
<div className='map-ui'>
<ul>
{this.renderSuperBlocks(superBlocks)}
<Spacer />
</ul>
</div>
);
}
}
Map.displayName = 'Map';
Map.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Map);

View File

@ -6,8 +6,8 @@ import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import store from 'store'; import store from 'store';
import { Map } from './Map'; import { Map } from './';
import mockNodes from '../../__mocks__/map-nodes'; import mockChallengeNodes from '../../__mocks__/challenge-nodes';
import mockIntroNodes from '../../__mocks__/intro-nodes'; import mockIntroNodes from '../../__mocks__/intro-nodes';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
@ -15,7 +15,7 @@ const renderer = new ShallowRenderer();
const baseProps = { const baseProps = {
introNodes: mockIntroNodes, introNodes: mockIntroNodes,
nodes: mockNodes, nodes: mockChallengeNodes,
toggleBlock: () => {}, toggleBlock: () => {},
toggleSuperBlock: () => {}, toggleSuperBlock: () => {},
resetExpansion: () => {} resetExpansion: () => {}
@ -25,7 +25,7 @@ test('<Map /> snapshot', () => {
const componentToRender = ( const componentToRender = (
<Map <Map
introNodes={mockIntroNodes} introNodes={mockIntroNodes}
nodes={mockNodes} nodes={mockChallengeNodes}
toggleBlock={() => {}} toggleBlock={() => {}}
toggleSuperBlock={() => {}} toggleSuperBlock={() => {}}
/> />
@ -45,7 +45,7 @@ describe('<Map/>', () => {
store.clearAll(); store.clearAll();
}); });
// 7 was chosen because it has a different superblock from the first node. // 7 was chosen because it has a different superblock from the first node.
const currentChallengeId = mockNodes[7].id; const currentChallengeId = mockChallengeNodes[7].id;
it('should expand the block with the most recent challenge', () => { it('should expand the block with the most recent challenge', () => {
const blockSpy = jest.fn(); const blockSpy = jest.fn();
@ -59,10 +59,10 @@ describe('<Map/>', () => {
const mapToRender = <Map {...props} />; const mapToRender = <Map {...props} />;
shallow(mapToRender); shallow(mapToRender);
expect(blockSpy).toHaveBeenCalledTimes(1); expect(blockSpy).toHaveBeenCalledTimes(1);
expect(blockSpy).toHaveBeenCalledWith(mockNodes[7].block); expect(blockSpy).toHaveBeenCalledWith(mockChallengeNodes[7].block);
expect(superSpy).toHaveBeenCalledTimes(1); expect(superSpy).toHaveBeenCalledTimes(1);
expect(superSpy).toHaveBeenCalledWith(mockNodes[7].superBlock); expect(superSpy).toHaveBeenCalledWith(mockChallengeNodes[7].superBlock);
}); });
it('should use the currentChallengeId prop if it exists', () => { it('should use the currentChallengeId prop if it exists', () => {
@ -85,13 +85,13 @@ describe('<Map/>', () => {
const mapToRender = <Map {...props} />; const mapToRender = <Map {...props} />;
shallow(mapToRender); shallow(mapToRender);
expect(blockSpy).toHaveBeenCalledTimes(1); expect(blockSpy).toHaveBeenCalledTimes(1);
expect(blockSpy).toHaveBeenCalledWith(mockNodes[0].block); expect(blockSpy).toHaveBeenCalledWith(mockChallengeNodes[0].block);
expect(superSpy).toHaveBeenCalledTimes(1); expect(superSpy).toHaveBeenCalledTimes(1);
expect(superSpy).toHaveBeenCalledWith(mockNodes[0].superBlock); expect(superSpy).toHaveBeenCalledWith(mockChallengeNodes[0].superBlock);
}); });
it('calls resetExpansion when initialising', () => { it('calls resetExpansion when initializing', () => {
const expansionSpy = jest.fn(); const expansionSpy = jest.fn();
const props = { const props = {
...baseProps, ...baseProps,

View File

@ -1,321 +1,334 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Map /> snapshot: Map 1`] = ` exports[`<Map /> snapshot: Map 1`] = `
<div <Row
className="map-ui" bsClass="row"
componentClass="div"
> >
<ul> <Col
<Connect(SuperBlock) bsClass="col"
introNodes={ componentClass="div"
Array [ sm={10}
Object { smOffset={1}
"fields": Object { xs={12}
"slug": "/super-block-one/block-a", >
}, <div
"frontmatter": Object { className="map-ui"
"block": "Block A", >
"title": "Introduction to Block A", <ul>
}, <Connect(SuperBlock)
}, introNodes={
Object { Array [
"fields": Object { Object {
"slug": "/super-block-one/block-b", "fields": Object {
}, "slug": "/super-block-one/block-a",
"frontmatter": Object { },
"block": "Block B", "frontmatter": Object {
"title": "Introduction to Block B", "block": "Block A",
}, "title": "Introduction to Block A",
}, },
Object { },
"fields": Object { Object {
"slug": "/super-block-one/block-c", "fields": Object {
}, "slug": "/super-block-one/block-b",
"frontmatter": Object { },
"block": "Block C", "frontmatter": Object {
"title": "Introduction to Block C", "block": "Block B",
}, "title": "Introduction to Block B",
}, },
] },
} Object {
nodes={ "fields": Object {
Array [ "slug": "/super-block-one/block-c",
Object { },
"block": "block-a", "frontmatter": Object {
"dashedName": "challenge-one", "block": "Block C",
"fields": Object { "title": "Introduction to Block C",
"blockName": "Block A", },
"slug": "/super-block-one/block-a/challenge-one", },
}, ]
"id": "a", }
"isPrivate": false, nodes={
"isRequired": false, Array [
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-a",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block A",
"dashedName": "challenge-two", "slug": "/super-block-one/block-a/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "a",
"slug": "/super-block-one/block-a/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "b", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge Two", "block": "block-a",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-b", "blockName": "Block A",
"dashedName": "challenge-one", "slug": "/super-block-one/block-a/challenge-two",
"fields": Object { },
"blockName": "Block B", "id": "b",
"slug": "/super-block-one/block-b/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "c", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-b",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-b", "blockName": "Block B",
"dashedName": "challenge-two", "slug": "/super-block-one/block-b/challenge-one",
"fields": Object { },
"blockName": "Block B", "id": "c",
"slug": "/super-block-one/block-b/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "d", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge Two", "block": "block-b",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-c", "blockName": "Block B",
"dashedName": "challenge-one", "slug": "/super-block-one/block-b/challenge-two",
"fields": Object { },
"blockName": "Block C", "id": "d",
"slug": "/super-block-one/block-c/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "e", "superBlock": "Super Block One",
"isPrivate": true, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-c",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block C",
"dashedName": "challenge-one", "slug": "/super-block-one/block-c/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "e",
"slug": "/super-block-one/block-a/challenge-one", "isPrivate": true,
}, "isRequired": false,
"id": "f", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge One", "block": "block-a",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block A",
"dashedName": "challenge-two", "slug": "/super-block-one/block-a/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "f",
"slug": "/super-block-one/block-a/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "g", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge Two", "block": "block-a",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-b", "blockName": "Block A",
"dashedName": "challenge-one", "slug": "/super-block-one/block-a/challenge-two",
"fields": Object { },
"blockName": "Block B", "id": "g",
"slug": "/super-block-one/block-b/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "h", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge One", "block": "block-b",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-b", "blockName": "Block B",
"dashedName": "challenge-two", "slug": "/super-block-one/block-b/challenge-one",
"fields": Object { },
"blockName": "Block B", "id": "h",
"slug": "/super-block-one/block-b/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "i", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge Two", "block": "block-b",
}, "dashedName": "challenge-two",
] "fields": Object {
} "blockName": "Block B",
superBlock="Super Block One" "slug": "/super-block-one/block-b/challenge-two",
/> },
<Connect(SuperBlock) "id": "i",
introNodes={ "isPrivate": false,
Array [ "isRequired": false,
Object { "superBlock": "Super Block Two",
"fields": Object { "title": "Challenge Two",
"slug": "/super-block-one/block-a", },
}, ]
"frontmatter": Object { }
"block": "Block A", superBlock="Super Block One"
"title": "Introduction to Block A", />
}, <Connect(SuperBlock)
}, introNodes={
Object { Array [
"fields": Object { Object {
"slug": "/super-block-one/block-b", "fields": Object {
}, "slug": "/super-block-one/block-a",
"frontmatter": Object { },
"block": "Block B", "frontmatter": Object {
"title": "Introduction to Block B", "block": "Block A",
}, "title": "Introduction to Block A",
}, },
Object { },
"fields": Object { Object {
"slug": "/super-block-one/block-c", "fields": Object {
}, "slug": "/super-block-one/block-b",
"frontmatter": Object { },
"block": "Block C", "frontmatter": Object {
"title": "Introduction to Block C", "block": "Block B",
}, "title": "Introduction to Block B",
}, },
] },
} Object {
nodes={ "fields": Object {
Array [ "slug": "/super-block-one/block-c",
Object { },
"block": "block-a", "frontmatter": Object {
"dashedName": "challenge-one", "block": "Block C",
"fields": Object { "title": "Introduction to Block C",
"blockName": "Block A", },
"slug": "/super-block-one/block-a/challenge-one", },
}, ]
"id": "a", }
"isPrivate": false, nodes={
"isRequired": false, Array [
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-a",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block A",
"dashedName": "challenge-two", "slug": "/super-block-one/block-a/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "a",
"slug": "/super-block-one/block-a/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "b", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge Two", "block": "block-a",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-b", "blockName": "Block A",
"dashedName": "challenge-one", "slug": "/super-block-one/block-a/challenge-two",
"fields": Object { },
"blockName": "Block B", "id": "b",
"slug": "/super-block-one/block-b/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "c", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-b",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-b", "blockName": "Block B",
"dashedName": "challenge-two", "slug": "/super-block-one/block-b/challenge-one",
"fields": Object { },
"blockName": "Block B", "id": "c",
"slug": "/super-block-one/block-b/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "d", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge Two", "block": "block-b",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-c", "blockName": "Block B",
"dashedName": "challenge-one", "slug": "/super-block-one/block-b/challenge-two",
"fields": Object { },
"blockName": "Block C", "id": "d",
"slug": "/super-block-one/block-c/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "e", "superBlock": "Super Block One",
"isPrivate": true, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block One", Object {
"title": "Challenge One", "block": "block-c",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block C",
"dashedName": "challenge-one", "slug": "/super-block-one/block-c/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "e",
"slug": "/super-block-one/block-a/challenge-one", "isPrivate": true,
}, "isRequired": false,
"id": "f", "superBlock": "Super Block One",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge One", "block": "block-a",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-a", "blockName": "Block A",
"dashedName": "challenge-two", "slug": "/super-block-one/block-a/challenge-one",
"fields": Object { },
"blockName": "Block A", "id": "f",
"slug": "/super-block-one/block-a/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "g", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge Two", "block": "block-a",
}, "dashedName": "challenge-two",
Object { "fields": Object {
"block": "block-b", "blockName": "Block A",
"dashedName": "challenge-one", "slug": "/super-block-one/block-a/challenge-two",
"fields": Object { },
"blockName": "Block B", "id": "g",
"slug": "/super-block-one/block-b/challenge-one", "isPrivate": false,
}, "isRequired": false,
"id": "h", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge Two",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge One", "block": "block-b",
}, "dashedName": "challenge-one",
Object { "fields": Object {
"block": "block-b", "blockName": "Block B",
"dashedName": "challenge-two", "slug": "/super-block-one/block-b/challenge-one",
"fields": Object { },
"blockName": "Block B", "id": "h",
"slug": "/super-block-one/block-b/challenge-two", "isPrivate": false,
}, "isRequired": false,
"id": "i", "superBlock": "Super Block Two",
"isPrivate": false, "title": "Challenge One",
"isRequired": false, },
"superBlock": "Super Block Two", Object {
"title": "Challenge Two", "block": "block-b",
}, "dashedName": "challenge-two",
] "fields": Object {
} "blockName": "Block B",
superBlock="Super Block Two" "slug": "/super-block-one/block-b/challenge-two",
/> },
<Spacer /> "id": "i",
</ul> "isPrivate": false,
</div> "isRequired": false,
"superBlock": "Super Block Two",
"title": "Challenge Two",
},
]
}
superBlock="Super Block Two"
/>
<Spacer />
</ul>
</div>
</Col>
</Row>
`; `;

View File

@ -7,7 +7,7 @@ import Adapter from 'enzyme-adapter-react-16';
import sinon from 'sinon'; import sinon from 'sinon';
import { Block } from './Block'; import { Block } from './Block';
import mockNodes from '../../../__mocks__/map-nodes'; import mockChallengeNodes from '../../../__mocks__/challenge-nodes';
import mockIntroNodes from '../../../__mocks__/intro-nodes'; import mockIntroNodes from '../../../__mocks__/intro-nodes';
import mockCompleted from '../../../__mocks__/completedChallengesMock'; import mockCompleted from '../../../__mocks__/completedChallengesMock';
@ -20,7 +20,7 @@ test('<Block /> not expanded snapshot', () => {
const componentToRender = ( const componentToRender = (
<Block <Block
blockDashedName='block-a' blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')} challenges={mockChallengeNodes.filter(node => node.block === 'block-a')}
completedChallenges={mockCompleted} completedChallenges={mockCompleted}
intro={mockIntroNodes[0]} intro={mockIntroNodes[0]}
isExpanded={false} isExpanded={false}
@ -39,7 +39,7 @@ test('<Block expanded snapshot', () => {
const componentToRender = ( const componentToRender = (
<Block <Block
blockDashedName='block-a' blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')} challenges={mockChallengeNodes.filter(node => node.block === 'block-a')}
completedChallenges={mockCompleted} completedChallenges={mockCompleted}
intro={mockIntroNodes[0]} intro={mockIntroNodes[0]}
isExpanded={true} isExpanded={true}
@ -58,7 +58,7 @@ test('<Block /> should handle toggle clicks correctly', () => {
const toggleMapSpy = sinon.spy(); const toggleMapSpy = sinon.spy();
const props = { const props = {
blockDashedName: 'block-a', blockDashedName: 'block-a',
challenges: mockNodes.filter(node => node.block === 'block-a'), challenges: mockChallengeNodes.filter(node => node.block === 'block-a'),
completedChallenges: mockCompleted, completedChallenges: mockCompleted,
intro: mockIntroNodes[0], intro: mockIntroNodes[0],
isExpanded: false, isExpanded: false,

View File

@ -7,7 +7,7 @@ import Adapter from 'enzyme-adapter-react-16';
import sinon from 'sinon'; import sinon from 'sinon';
import { SuperBlock } from './SuperBlock'; import { SuperBlock } from './SuperBlock';
import mockNodes from '../../../__mocks__/map-nodes'; import mockChallengeNodes from '../../../__mocks__/challenge-nodes';
import mockIntroNodes from '../../../__mocks__/intro-nodes'; import mockIntroNodes from '../../../__mocks__/intro-nodes';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
@ -18,7 +18,7 @@ test('<SuperBlock /> not expanded snapshot', () => {
const props = { const props = {
introNodes: mockIntroNodes, introNodes: mockIntroNodes,
isExpanded: false, isExpanded: false,
nodes: mockNodes, nodes: mockChallengeNodes,
superBlock: 'Super Block One', superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy toggleSuperBlock: toggleSpy
}; };
@ -33,7 +33,7 @@ test('<SuperBlock /> expanded snapshot', () => {
const props = { const props = {
introNodes: mockIntroNodes, introNodes: mockIntroNodes,
isExpanded: true, isExpanded: true,
nodes: mockNodes, nodes: mockChallengeNodes,
superBlock: 'Super Block One', superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy toggleSuperBlock: toggleSpy
}; };
@ -48,7 +48,7 @@ test('<SuperBlock should handle toggle clicks correctly', () => {
const props = { const props = {
introNodes: mockIntroNodes, introNodes: mockIntroNodes,
isExpanded: false, isExpanded: false,
nodes: mockNodes, nodes: mockChallengeNodes,
superBlock: 'Super Block One', superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy toggleSuperBlock: toggleSpy
}; };

View File

@ -1 +1,104 @@
export default from './Map.js'; import React, { Component } from 'react';
import { Row, Col } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import uniq from 'lodash/uniq';
import { createSelector } from 'reselect';
import SuperBlock from './components/SuperBlock';
import Spacer from '../helpers/Spacer';
import './map.css';
import { ChallengeNode } from '../../redux/propTypes';
import { toggleSuperBlock, toggleBlock, resetExpansion } from './redux';
import { currentChallengeIdSelector } from '../../redux';
const propTypes = {
currentChallengeId: 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
})
})
),
nodes: PropTypes.arrayOf(ChallengeNode),
resetExpansion: PropTypes.func,
toggleBlock: PropTypes.func.isRequired,
toggleSuperBlock: PropTypes.func.isRequired
};
const mapStateToProps = state => {
return createSelector(
currentChallengeIdSelector,
currentChallengeId => ({
currentChallengeId
})
)(state);
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
resetExpansion,
toggleSuperBlock,
toggleBlock
},
dispatch
);
}
export class Map extends Component {
componentDidMount() {
this.initializeExpandedState(this.props.currentChallengeId);
}
initializeExpandedState(currentChallengeId) {
this.props.resetExpansion();
const { superBlock, block } = currentChallengeId
? this.props.nodes.find(node => node.id === currentChallengeId)
: this.props.nodes[0];
this.props.toggleBlock(block);
this.props.toggleSuperBlock(superBlock);
}
renderSuperBlocks(superBlocks) {
const { nodes, introNodes } = this.props;
return superBlocks.map(superBlock => (
<SuperBlock
introNodes={introNodes}
key={superBlock}
nodes={nodes}
superBlock={superBlock}
/>
));
}
render() {
const { nodes } = this.props;
const superBlocks = uniq(nodes.map(({ superBlock }) => superBlock));
return (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<div className='map-ui'>
<ul>
{this.renderSuperBlocks(superBlocks)}
<Spacer />
</ul>
</div>
</Col>
</Row>
);
}
}
Map.displayName = 'Map';
Map.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Map);

View File

@ -1,27 +1,40 @@
/* global expect */ /* global expect */
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';
import 'jest-dom/extend-expect'; import 'jest-dom/extend-expect';
import { IndexPage } from '../../pages'; import { LearnPage } from '../../pages/learn';
import Welcome from './'; import Welcome from './';
import mockChallengeNodes from '../../__mocks__/challenge-nodes';
import mockIntroNodes from '../../__mocks__/intro-nodes';
describe('<Welcome />', () => { describe('<Welcome />', () => {
it('renders when visiting index page and logged in', () => { it('renders when visiting index page and logged in', () => {
const container = renderer const shallow = new ShallowRenderer();
.create(<IndexPage {...loggedInProps} />) shallow.render(<LearnPage {...loggedInProps} />);
.toTree(); const result = shallow.getRenderOutput();
expect(container.rendered.type.displayName === 'Welcome').toBeTruthy(); expect(result.type.displayName === 'LearnLayout').toBeTruthy();
}); });
it('has four links', () => { it('has a header', () => {
const container = renderer.create(<Welcome name={'developmentuser'} />) const container = renderer.create(<Welcome name={'Development User'} />)
.root; .root;
expect(container.findAllByType('a').length === 4).toBeTruthy(); expect(container.findAllByType('h1').length === 1).toBeTruthy();
});
it('has a blockquote', () => {
const container = renderer.create(<Welcome name={'Development User'} />)
.root;
expect(container.findAllByType('blockquote').length === 1).toBeTruthy();
}); });
}); });
const nodes = mockChallengeNodes.map(node => {
return { node };
});
const loggedInProps = { const loggedInProps = {
fetchState: { fetchState: {
complete: true, complete: true,
@ -31,7 +44,17 @@ const loggedInProps = {
}, },
isSignedIn: true, isSignedIn: true,
user: { user: {
acceptedPrivacyTerms: true, name: 'Development User'
username: 'developmentuser' },
data: {
challengeNode: {
fields: {
slug:
// eslint-disable-next-line max-len
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements'
}
},
allChallengeNode: { edges: nodes },
allMarkdownRemark: { edges: [{ mdEdges: mockIntroNodes }] }
} }
}; };

View File

@ -1,9 +1,8 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap'; import { Row, Col } from '@freecodecamp/react-bootstrap';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Spacer, Link } from '../helpers'; import { Spacer } from '../helpers';
import { randomQuote } from '../../utils/get-words'; import { randomQuote } from '../../utils/get-words';
import './welcome.css'; import './welcome.css';
@ -12,68 +11,35 @@ function Welcome({ name }) {
const { quote, author } = randomQuote(); const { quote, author } = randomQuote();
return ( return (
<Fragment> <Fragment>
<Helmet> <Row>
<title>Welcome | freeCodeCamp.org</title> <Col sm={10} smOffset={1} xs={12}>
</Helmet>
<main>
<Grid>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<h1 className='text-center big-heading'>
Welcome {name ? name : 'Camper'}!
</h1>
</Col>
</Row>
<Spacer /> <Spacer />
<Row className='text-center quote-partial'> <h1 className='text-center big-heading'>
<Col sm={10} smOffset={1} xs={12}> {name ? 'Welcome back ' + name : 'Welcome to freeCodeCamp.org'}
<blockquote className='blockquote'> </h1>
<span> </Col>
<q>{quote}</q> </Row>
<footer className='quote-author blockquote-footer'> <Spacer />
<cite>{author}</cite> <Row className='text-center quote-partial'>
</footer> <Col sm={10} smOffset={1} xs={12}>
</span> <blockquote className='blockquote'>
</blockquote> <span>
</Col> <q>{quote}</q>
</Row> <footer className='quote-author blockquote-footer'>
<Row> <cite>{author}</cite>
<Col sm={10} smOffset={1} xs={12}> </footer>
<Spacer /> </span>
<h2 className='text-center medium-heading'> </blockquote>
What would you like to do today? </Col>
</h2> </Row>
</Col> <Row>
</Row> <Col sm={10} smOffset={1} xs={12}>
<Spacer /> <Spacer />
<Row> <h2 className='text-center medium-heading'>
<Col sm={10} smOffset={1} xs={12}> What would you like to do today?
<Link className='btn btn-lg btn-primary btn-block' to='/learn'> </h2>
Build Projects and Earn Certifications </Col>
</Link> </Row>
<Link className='btn btn-lg btn-primary btn-block' to='/settings'>
Update Your Developer Portfolio
</Link>
<Link
className='btn btn-lg btn-primary btn-block'
external={true}
to='/news'
>
Read Developer News
</Link>
<Link
className='btn btn-lg btn-primary btn-block'
external={true}
to='/forum'
>
Help Developers on the Forum
</Link>
</Col>
</Row>
<Spacer size={4} />
</Grid>
</main>
</Fragment> </Fragment>
); );
} }

View File

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Loader } from '../components/helpers'; import { Loader } from '../components/helpers';
import Welcome from '../components/welcome';
import Landing from '../components/landing'; import Landing from '../components/landing';
import { import {
userSelector, userSelector,
@ -25,11 +24,12 @@ const mapStateToProps = createSelector(
); );
const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms'); const RedirectAcceptPrivacyTerm = createRedirect('/accept-privacy-terms');
const RedirectLearn = createRedirect('/learn');
export const IndexPage = ({ export const IndexPage = ({
fetchState: { pending, complete }, fetchState: { pending, complete },
isSignedIn, isSignedIn,
user: { acceptedPrivacyTerms, name = '' } user: { acceptedPrivacyTerms }
}) => { }) => {
if (pending && !complete) { if (pending && !complete) {
return <Loader fullScreen={true} />; return <Loader fullScreen={true} />;
@ -40,7 +40,7 @@ export const IndexPage = ({
} }
if (isSignedIn) { if (isSignedIn) {
return <Welcome name={name} />; return <RedirectLearn />;
} }
return <Landing />; return <Landing />;
@ -54,14 +54,7 @@ const propTypes = {
}), }),
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
user: PropTypes.shape({ user: PropTypes.shape({
acceptedPrivacyTerms: PropTypes.bool, acceptedPrivacyTerms: PropTypes.bool
completedCertCount: PropTypes.number,
completedChallengeCount: PropTypes.number,
completedLegacyCertCount: PropTypes.number,
completedProjectCount: PropTypes.number,
isDonating: PropTypes.bool,
name: PropTypes.string,
username: PropTypes.string
}) })
}; };

View File

@ -1,22 +0,0 @@
.learn-page-wrapper {
display: flex;
flex-direction: column;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
top: 38px;
}
.learn-page-wrapper .signup-btn {
width: 100%;
}
@media screen and (max-width: 630px) {
.learn-page-wrapper {
padding: 0 40px;
}
}
.map-ui p {
margin-bottom: 0rem;
}

View File

@ -1,19 +1,22 @@
import React from 'react'; import React from 'react';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Row, Col } from '@freecodecamp/react-bootstrap';
import { userFetchStateSelector, isSignedInSelector } from '../redux'; import {
userFetchStateSelector,
isSignedInSelector,
userSelector
} from '../redux';
import LearnLayout from '../components/layouts/Learn'; import LearnLayout from '../components/layouts/Learn';
import Login from '../components/Header/components/Login'; import Login from '../components/Header/components/Login';
import { Link, Spacer, Loader } from '../components/helpers'; import { Link, Spacer, Loader } from '../components/helpers';
import Map from '../components/Map'; import Map from '../components/Map';
import Welcome from '../components/welcome';
import './learn.css';
import { import {
ChallengeNode, ChallengeNode,
@ -24,9 +27,11 @@ import {
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userFetchStateSelector, userFetchStateSelector,
isSignedInSelector, isSignedInSelector,
(fetchState, isSignedIn) => ({ userSelector,
(fetchState, isSignedIn, user) => ({
fetchState, fetchState,
isSignedIn isSignedIn,
user
}) })
); );
@ -41,17 +46,20 @@ const propTypes = {
complete: PropTypes.bool, complete: PropTypes.bool,
errored: PropTypes.bool errored: PropTypes.bool
}), }),
isSignedIn: PropTypes.bool isSignedIn: PropTypes.bool,
user: PropTypes.shape({
name: PropTypes.string
})
}; };
const BigCallToAction = isSignedIn => { const BigCallToAction = isSignedIn => {
if (!isSignedIn) { if (!isSignedIn) {
return ( return (
<> <>
<Spacer size={2} />
<Row> <Row>
<Col sm={8} smOffset={2} xs={12}> <Col sm={10} smOffset={1} xs={12}>
<Login className={'text-center'}>Sign in to save progress.</Login> <Spacer />
<Login>Sign in to save your progress.</Login>
</Col> </Col>
</Row> </Row>
</> </>
@ -60,9 +68,10 @@ const BigCallToAction = isSignedIn => {
return ''; return '';
}; };
const IndexPage = ({ export const LearnPage = ({
fetchState: { pending, complete }, fetchState: { pending, complete },
isSignedIn, isSignedIn,
user: { name = '' },
data: { data: {
challengeNode: { challengeNode: {
fields: { slug } fields: { slug }
@ -77,44 +86,34 @@ const IndexPage = ({
return ( return (
<LearnLayout> <LearnLayout>
<div className='learn-page-wrapper'> <Helmet title='Learn | freeCodeCamp.org' />
<Helmet title='Learn | freeCodeCamp.org' /> <Grid>
{BigCallToAction(isSignedIn)} <Welcome name={name} />
<Spacer size={2} /> <Row className='text-center'>
<h1 className='text-center'>Welcome to the freeCodeCamp curriculum</h1> <Col sm={10} smOffset={1} xs={12}>
<p> {BigCallToAction(isSignedIn)}
We have thousands of coding lessons to help you improve your skills. <Spacer />
</p> <h3>
<p> If you are new to coding, we recommend you{' '}
You can earn each certification by completing its 5 final projects. <Link to={slug}>start at the beginning</Link>.
</p> </h3>
<p> </Col>
And yes - all of this is 100% free, thanks to the thousands of campers </Row>
who{' '}
<Link external={true} to='/donate'>
donate
</Link>{' '}
to our nonprofit.
</p>
<p>
If you are new to coding, we recommend you{' '}
<Link to={slug}>start at the beginning</Link>.
</p>
<Map <Map
introNodes={mdEdges.map(({ node }) => node)} introNodes={mdEdges.map(({ node }) => node)}
nodes={edges nodes={edges
.map(({ node }) => node) .map(({ node }) => node)
.filter(({ isPrivate }) => !isPrivate)} .filter(({ isPrivate }) => !isPrivate)}
/> />
</div> </Grid>
</LearnLayout> </LearnLayout>
); );
}; };
IndexPage.displayName = 'IndexPage'; LearnPage.displayName = 'LearnPage';
IndexPage.propTypes = propTypes; LearnPage.propTypes = propTypes;
export default connect(mapStateToProps)(IndexPage); export default connect(mapStateToProps)(LearnPage);
export const query = graphql` export const query = graphql`
query FirstChallenge { query FirstChallenge {