diff --git a/client/src/client-only-routes/ShowUser.js b/client/src/client-only-routes/ShowUser.js index 225f95e748..e04745388a 100644 --- a/client/src/client-only-routes/ShowUser.js +++ b/client/src/client-only-routes/ShowUser.js @@ -116,12 +116,12 @@ class ShowUser extends Component { {t('report.portfolio')} | freeCodeCamp.org - +

{t('report.portfolio-2', { username: username })}

- +

diff --git a/client/src/components/Footer/footer.css b/client/src/components/Footer/footer.css index 1b0d18d772..65155076e9 100644 --- a/client/src/components/Footer/footer.css +++ b/client/src/components/Footer/footer.css @@ -2,7 +2,7 @@ /* ---------------------------------------------------------- */ .site-footer { - position: relative; + flex-shrink: 0; color: var(--tertiary-color); background: var(--tertiary-background); line-height: 1.6; diff --git a/client/src/components/Map/Map.test.js b/client/src/components/Map/Map.test.js index 6c8c549e30..2170f3b927 100644 --- a/client/src/components/Map/Map.test.js +++ b/client/src/components/Map/Map.test.js @@ -1,145 +1,29 @@ /* global expect jest */ import React from 'react'; +import { useStaticQuery } from 'gatsby'; import { render } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { createStore } from '../../redux/createStore'; import { Map } from './'; import mockChallengeNodes from '../../__mocks__/challenge-nodes'; -import mockIntroNodes from '../../__mocks__/intro-nodes'; -import { dasherize } from '../../../../utils/slugs'; - -function renderWithRedux(ui) { - return render({ui}); -} - -const baseProps = { - introNodes: mockIntroNodes, - nodes: mockChallengeNodes, - toggleBlock: () => {}, - toggleSuperBlock: () => {}, - resetExpansion: () => {}, - isSignedIn: true -}; +beforeEach(() => { + useStaticQuery.mockImplementationOnce(() => ({ + allChallengeNode: { + nodes: mockChallengeNodes + } + })); +}); // set .scrollTo to avoid errors in default test environment window.scrollTo = jest.fn(); test(' snapshot', () => { - const { container } = renderWithRedux( - {}} - toggleBlock={() => {}} - toggleSuperBlock={() => {}} - /> - ); + const { container } = render(); expect(container).toMatchSnapshot('Map'); }); -describe('', () => { - describe('after reload', () => { - 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(); - - 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(); - - 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(); - - 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(); - - 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(); - - expect(expansionSpy).toHaveBeenCalledTimes(1); - }); - }); -}); +const props = { + forLanding: true +}; diff --git a/client/src/components/Map/__snapshots__/Map.test.js.snap b/client/src/components/Map/__snapshots__/Map.test.js.snap index cee46e039d..9ae8b88e7e 100644 --- a/client/src/components/Map/__snapshots__/Map.test.js.snap +++ b/client/src/components/Map/__snapshots__/Map.test.js.snap @@ -3,92 +3,310 @@ exports[` snapshot: Map 1`] = `

`; diff --git a/client/src/components/Map/components/SuperBlock.js b/client/src/components/Map/components/SuperBlock.js deleted file mode 100644 index 72073cbe4b..0000000000 --- a/client/src/components/Map/components/SuperBlock.js +++ /dev/null @@ -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 ( -
    - {blockDashedNames.map(blockDashedName => ( - node.block === blockDashedName - )} - intro={find( - introNodes, - ({ frontmatter: { block } }) => - block - .toLowerCase() - .split(' ') - .join('-') === blockDashedName - )} - key={blockDashedName} - /> - ))} -
- ); - } - - render() { - const { superBlock, isExpanded, toggleSuperBlock } = this.props; - return ( -
  • - - {isExpanded ? this.renderBlock(superBlock) : null} -
  • - ); - } -} - -SuperBlock.displayName = 'SuperBlock'; -SuperBlock.propTypes = propTypes; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(SuperBlock); diff --git a/client/src/components/Map/components/SuperBlock.test.js b/client/src/components/Map/components/SuperBlock.test.js deleted file mode 100644 index 6cecea7493..0000000000 --- a/client/src/components/Map/components/SuperBlock.test.js +++ /dev/null @@ -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({ui}); -} - -test(' not expanded snapshot', () => { - const props = { - introNodes: mockIntroNodes, - isExpanded: false, - nodes: mockChallengeNodes, - superBlock: 'Super Block One', - toggleSuperBlock: () => {} - }; - - const { container } = render(); - - expect(container).toMatchSnapshot('superBlock-not-expanded'); -}); - -test(' expanded snapshot', () => { - const props = { - introNodes: mockIntroNodes, - isExpanded: true, - nodes: mockChallengeNodes, - superBlock: 'Super Block One', - toggleSuperBlock: () => {} - }; - - const { container } = renderWithRedux(); - - expect(container).toMatchSnapshot('superBlock-expanded'); -}); - -test(' { - 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( - , - 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( - - - - ); - - expect(container.querySelector('ul')).toBeInTheDocument(); -}); diff --git a/client/src/components/Map/components/__snapshots__/SuperBlock.test.js.snap b/client/src/components/Map/components/__snapshots__/SuperBlock.test.js.snap deleted file mode 100644 index 472299e363..0000000000 --- a/client/src/components/Map/components/__snapshots__/SuperBlock.test.js.snap +++ /dev/null @@ -1,242 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` expanded snapshot: superBlock-expanded 1`] = ` -
    -
  • - -
      -
    • - -
        - -
      • - -
          - -
        • - -
            - -
          -
        • -
  • -`; - -exports[` not expanded snapshot: superBlock-not-expanded 1`] = ` -
    -
  • - -
  • -
    -`; diff --git a/client/src/components/Map/index.js b/client/src/components/Map/index.js index 7808b139de..7f1b7f4a59 100644 --- a/client/src/components/Map/index.js +++ b/client/src/components/Map/index.js @@ -1,138 +1,98 @@ -import React, { Component } from 'react'; +import React 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 { graphql, useStaticQuery } from 'gatsby'; -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'; +import { Link } from '../helpers'; +import LinkButton from '../../assets/icons/LinkButton'; import { dasherize } from '../../../../utils/slugs'; +import './map.css'; const propTypes = { - currentChallengeId: PropTypes.string, - 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 + forLanding: PropTypes.bool }; -const mapStateToProps = state => { - return createSelector( - currentChallengeIdSelector, - currentChallengeId => ({ - currentChallengeId - }) - )(state); -}; +const codingPrepRE = new RegExp('Interview Prep'); -function mapDispatchToProps(dispatch) { - return bindActionCreators( - { - resetExpansion, - toggleSuperBlock, - toggleBlock - }, - dispatch +function createSuperBlockTitle(str) { + return codingPrepRE.test(str) + ? `${str} (Thousands of hours of challenges)` + : `${str} Certification (300\xa0hours)`; +} + +function renderLandingMap(nodes) { + return ( +
      + {nodes.map((node, i) => ( +
    • + + {node.superBlock} + + +
    • + ))} +
    ); } -export class Map extends Component { - constructor(props) { - super(props); - this.initializeExpandedState(); - } +function renderLearnMap(nodes) { + return ( + + +
      + {nodes.map((node, i) => ( +
    • + + {createSuperBlockTitle(node.superBlock)} + +
    • + ))} +
    + +
    + ); +} - // As this happens in the constructor, it's necessary to manipulate state - // directly. - initializeExpandedState() { - const { - currentChallengeId, - hash, - nodes, - resetExpansion, - toggleBlock, - toggleSuperBlock, - isSignedIn - } = this.props; - resetExpansion(); - - let node; - - // 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); +export function Map({ forLanding = false }) { + /* + * this query gets the first challenge from each block and the second block + * from each superblock, leaving you with one challenge from each + * superblock + */ + const data = useStaticQuery(graphql` + query SuperBlockNodes { + allChallengeNode( + sort: { fields: [superOrder] } + filter: { order: { eq: 2 }, challengeOrder: { eq: 1 } } + ) { + nodes { + superBlock + dashedName + } } - if (!node) node = nodes[0]; } + `); - if (!node) return; + let nodes = data.allChallengeNode.nodes; - toggleBlock(node.block); - toggleSuperBlock(node.superBlock); + if (forLanding) { + nodes = nodes.filter(node => node.superBlock !== 'Coding Interview Prep'); } - renderSuperBlocks(superBlocks) { - const { nodes, introNodes } = this.props; - return superBlocks.map(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 ( - - -
    -
      - {this.renderSuperBlocks(superBlocks)} - -
    -
    - -
    - ); - } + return ( +
    + {forLanding ? renderLandingMap(nodes) : renderLearnMap(nodes)} +
    + ); } Map.displayName = 'Map'; Map.propTypes = propTypes; -export default connect( - mapStateToProps, - mapDispatchToProps -)(Map); +export default Map; diff --git a/client/src/components/Map/map.css b/client/src/components/Map/map.css index 1cef543537..46728fd19f 100644 --- a/client/src/components/Map/map.css +++ b/client/src/components/Map/map.css @@ -28,6 +28,24 @@ button.map-title:hover { 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 { margin-bottom: 0; } @@ -38,7 +56,7 @@ button.map-title:hover { padding-left: 20px; } -.superblock .map-title svg { +.map-title svg { width: 14px; margin-right: 5px; flex-shrink: 0; @@ -86,7 +104,14 @@ li.open > .map-title svg:first-child { text-decoration: none; } -/* 14px is the width of the expansion arrow */ -.superblock { +.block { max-width: calc(100% - 14px); } + +.block svg { + width: 14px; + margin-right: 5px; + flex-shrink: 0; + fill: var(--color-quaternary) !important; + stroke: var(--color-quaternary); +} diff --git a/client/src/components/Map/redux/index.js b/client/src/components/Map/redux/index.js index b31e799012..27cc04c0f6 100644 --- a/client/src/components/Map/redux/index.js +++ b/client/src/components/Map/redux/index.js @@ -1,29 +1,19 @@ import { createAction, handleActions } from 'redux-actions'; - import { createTypes } from '../../../../utils/stateManagement'; export const ns = 'curriculumMap'; -export const getNS = () => ns; - const initialState = { expandedState: { - superBlock: {}, block: {} } }; -const types = createTypes( - ['resetExpansion', 'toggleSuperBlock', 'toggleBlock'], - ns -); +const types = createTypes(['resetExpansion', 'toggleBlock'], ns); export const resetExpansion = createAction(types.resetExpansion); 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 => !!state[ns].expandedState.block[block]; @@ -32,7 +22,6 @@ export const reducer = handleActions( [types.resetExpansion]: state => ({ ...state, expandedState: { - superBlock: {}, block: {} } }), @@ -45,16 +34,6 @@ export const reducer = handleActions( [payload]: !state.expandedState.block[payload] } } - }), - [types.toggleSuperBlock]: (state, { payload }) => ({ - ...state, - expandedState: { - ...state.expandedState, - superBlock: { - ...state.expandedState.superBlock, - [payload]: !state.expandedState.superBlock[payload] - } - } }) }, initialState diff --git a/client/src/components/landing/components/Certifications.js b/client/src/components/landing/components/Certifications.js index c384674f55..103a3e9aa1 100644 --- a/client/src/components/landing/components/Certifications.js +++ b/client/src/components/landing/components/Certifications.js @@ -1,41 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Col, Row } from '@freecodecamp/react-bootstrap'; -import { uniq } from 'lodash'; -import { Spacer, Link } from '../../helpers'; -import LinkButton from '../../../assets/icons/LinkButton'; +import { Spacer } from '../../helpers'; import BigCallToAction from './BigCallToAction'; import { useTranslation } from 'react-i18next'; +import Map from '../../Map/index'; const propTypes = { - nodes: PropTypes.array, page: PropTypes.string }; -const Certifications = ({ nodes, page }) => { +const Certifications = ({ page = 'landing' }) => { const { t } = useTranslation(); - const superBlocks = uniq(nodes.map(node => node.superBlock)).filter( - cert => cert !== 'Coding Interview Prep' - ); return ( -

    {t('landing.certification-heading')}

    -
      - {superBlocks.map((superBlock, i) => ( -
    • - - {superBlock} - - -
    • - ))} -
    +

    {t('landing.certification-heading')}

    + diff --git a/client/src/components/landing/index.js b/client/src/components/landing/index.js index c3552f7070..46aafd4ec5 100644 --- a/client/src/components/landing/index.js +++ b/client/src/components/landing/index.js @@ -2,7 +2,6 @@ import React, { Fragment } from 'react'; import { Grid } from '@freecodecamp/react-bootstrap'; import Helmet from 'react-helmet'; import PropTypes from 'prop-types'; -import { graphql, useStaticQuery } from 'gatsby'; import { useTranslation } from 'react-i18next'; import Testimonials from './components/Testimonials'; @@ -19,18 +18,6 @@ const propTypes = { export const Landing = ({ page = 'landing' }) => { const { t } = useTranslation(); - const data = useStaticQuery(graphql` - query certifications { - challenges: allChallengeNode( - sort: { fields: [superOrder, order, challengeOrder] } - ) { - nodes { - superBlock - } - } - } - `); - return ( @@ -45,7 +32,7 @@ export const Landing = ({ page = 'landing' }) => { - + diff --git a/client/src/components/layouts/Default.js b/client/src/components/layouts/Default.js index 7d7d355eea..f92c3858a4 100644 --- a/client/src/components/layouts/Default.js +++ b/client/src/components/layouts/Default.js @@ -1,4 +1,4 @@ -import React, { Fragment, Component } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -142,7 +142,7 @@ class DefaultLayout extends Component { } = this.props; return ( - +
    {fontawesome.dom.css()} -
    +
    {hasMessage && flashMessage ? ( ) : null} {children} - {showFooter &&
    }
    + {showFooter &&
    } - +
    ); } } diff --git a/client/src/components/layouts/global.css b/client/src/components/layouts/global.css index 5e464f1d11..ce43ab1291 100644 --- a/client/src/components/layouts/global.css +++ b/client/src/components/layouts/global.css @@ -1,14 +1,30 @@ html { + height: 100%; font-size: 18px; -webkit-font-smoothing: antialiased; } body { + height: 100%; font-family: 'Roboto Mono', monospace; color: var(--secondary-color); background: var(--secondary-background); } +#___gatsby { + height: 100%; +} + +#gatsby-focus-wrapper { + height: 100%; +} + +.page-wrapper { + height: 100%; + display: flex; + flex-direction: column; +} + .btn-cta-big { max-height: 100%; font-size: 1.5rem; @@ -16,12 +32,20 @@ body { 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 { + flex: 1 0 auto; margin-top: var(--header-height, 0px); background: var(--secondary-background); - display: flex; - flex-direction: column; - min-height: calc(100vh - var(--header-height, 0px)); } h1 { diff --git a/client/src/pages/learn.js b/client/src/pages/learn.js index 75709625a7..2494891139 100644 --- a/client/src/pages/learn.js +++ b/client/src/pages/learn.js @@ -7,8 +7,8 @@ import Helmet from 'react-helmet'; import { connect } from 'react-redux'; import { useTranslation } from 'react-i18next'; +import { Spacer } from '../components/helpers'; import LearnLayout from '../components/layouts/Learn'; -import { dasherize } from '../../../utils/slugs'; import Map from '../components/Map'; import Intro from '../components/Intro'; import { @@ -16,11 +16,7 @@ import { isSignedInSelector, userSelector } from '../redux'; -import { - ChallengeNode, - AllChallengeNode, - AllMarkdownRemark -} from '../redux/propTypes'; +import { ChallengeNode } from '../redux/propTypes'; const mapStateToProps = createSelector( userFetchStateSelector, @@ -35,18 +31,14 @@ const mapStateToProps = createSelector( const propTypes = { data: PropTypes.shape({ - challengeNode: ChallengeNode, - allChallengeNode: AllChallengeNode, - allMarkdownRemark: AllMarkdownRemark + challengeNode: ChallengeNode }), fetchState: PropTypes.shape({ pending: PropTypes.bool, complete: PropTypes.bool, errored: PropTypes.bool }), - hash: PropTypes.string, isSignedIn: PropTypes.bool, - location: PropTypes.object, state: PropTypes.object, user: PropTypes.shape({ 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 = ({ - location: { hash = '', state = '' }, isSignedIn, fetchState: { pending, complete }, user: { name = '', completedChallengeCount = 0 }, data: { challengeNode: { fields: { slug } - }, - allChallengeNode: { edges }, - allMarkdownRemark: { edges: mdEdges } + } } }) => { const { t } = useTranslation(); - const hashValue = hashValueSelector(state, hash); + return ( @@ -89,14 +71,8 @@ export const LearnPage = ({ pending={pending} slug={slug} /> - node)} - isSignedIn={isSignedIn} - nodes={edges - .map(({ node }) => node) - .filter(({ isPrivate }) => !isPrivate)} - /> + + ); @@ -114,33 +90,5 @@ export const query = graphql` 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 - } - } - } - } } `; diff --git a/client/src/pages/learn/coding-interview-prep/index.md b/client/src/pages/learn/coding-interview-prep/index.md new file mode 100644 index 0000000000..9fa8cd1b91 --- /dev/null +++ b/client/src/pages/learn/coding-interview-prep/index.md @@ -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). diff --git a/client/src/pages/learn/information-security/python-for-penetration-testing/index.md b/client/src/pages/learn/information-security/python-for-penetration-testing/index.md index d6b9870b46..f785d62095 100644 --- a/client/src/pages/learn/information-security/python-for-penetration-testing/index.md +++ b/client/src/pages/learn/information-security/python-for-penetration-testing/index.md @@ -1,7 +1,7 @@ --- title: Introduction to the Python for Penetration Testing Lectures block: Python for Penetration Testing -superBlock: Data Analysis with Python +superBlock: Information Security --- ## Introduction to the Python for Penetration Testing Challenges diff --git a/client/src/pages/learn/machine-learning-with-python/how-neural-networks-work/index.md b/client/src/pages/learn/machine-learning-with-python/how-neural-networks-work/index.md index 052fe98e01..d027c3c1de 100644 --- a/client/src/pages/learn/machine-learning-with-python/how-neural-networks-work/index.md +++ b/client/src/pages/learn/machine-learning-with-python/how-neural-networks-work/index.md @@ -1,7 +1,7 @@ --- title: Introduction to the How Neural Networks Work Lectures block: How Neural Networks Work -superBlock: Data Analysis with Python +superBlock: Machine Learning with Python --- ## Introduction to the How Neural Networks Work Challenges diff --git a/client/src/templates/Introduction/SuperBlockIntro.js b/client/src/templates/Introduction/SuperBlockIntro.js index 85619ae79d..ab7ffb56fe 100644 --- a/client/src/templates/Introduction/SuperBlockIntro.js +++ b/client/src/templates/Introduction/SuperBlockIntro.js @@ -1,49 +1,206 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import Helmet from 'react-helmet'; 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 { MarkdownRemark } from '../../redux/propTypes'; +import Block from '../../components/Map/components/Block'; +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 = { + currentChallengeId: PropTypes.string, 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 { - html, - frontmatter: { superBlock } - } = markdownRemark; - return ( - - - {superBlock} | freeCodeCamp.org - - -
    - - +const mapStateToProps = state => { + return createSelector( + currentChallengeIdSelector, + isSignedInSelector, + (currentChallengeId, isSignedIn) => ({ + currentChallengeId, + isSignedIn + }) + )(state); +}; + +const mapDispatchToProps = dispatch => + bindActionCreators( + { resetExpansion, toggleBlock: b => toggleBlock(b) }, + dispatch ); + +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 ( +
      + {blockDashedNames.map(blockDashedName => ( + node.block === blockDashedName + )} + intro={find( + introNodes, + ({ frontmatter: { block } }) => + block + .toLowerCase() + .split(' ') + .join('-') === blockDashedName + )} + key={blockDashedName} + /> + ))} +
    + ); +} + +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 ( + + + {superBlock} | freeCodeCamp.org + + + +

    + {superBlock} + {superBlock !== 'Coding Interview Prep' ? ' Certification' : ''} +

    + +
    + {renderBlock( + edges.map(({ node }) => node), + mdEdges.map(({ node }) => node) + )} +
    + +
    +
    + ); + } } SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage'; SuperBlockIntroductionPage.propTypes = propTypes; -export default SuperBlockIntroductionPage; +export default connect( + mapStateToProps, + mapDispatchToProps +)(SuperBlockIntroductionPage); export const query = graphql` - query SuperBlockIntroPageBySlug($slug: String!) { + query SuperBlockIntroPageBySlug($slug: String!, $superBlock: String!) { markdownRemark(fields: { slug: { eq: $slug } }) { frontmatter { 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 + } + } + } } } `; diff --git a/client/src/templates/Introduction/intro.css b/client/src/templates/Introduction/intro.css index c83ab7707a..0337bce8c8 100644 --- a/client/src/templates/Introduction/intro.css +++ b/client/src/templates/Introduction/intro.css @@ -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) { .intro-layout-container { padding: 0 10px; diff --git a/client/utils/gatsby/challengePageCreator.js b/client/utils/gatsby/challengePageCreator.js index ad37dfcca5..d7995931fe 100644 --- a/client/utils/gatsby/challengePageCreator.js +++ b/client/utils/gatsby/challengePageCreator.js @@ -122,7 +122,7 @@ exports.createSuperBlockIntroPages = createPage => edge => { path: slug, component: superBlockIntro, context: { - superBlock: dasherize(superBlock), + superBlock: superBlock, slug } }); diff --git a/client/utils/gatsby/layoutSelector.js b/client/utils/gatsby/layoutSelector.js index 6fa4a90c9b..c3a61387d6 100644 --- a/client/utils/gatsby/layoutSelector.js +++ b/client/utils/gatsby/layoutSelector.js @@ -11,6 +11,7 @@ export default function layoutSelector({ element, props }) { const { location: { pathname } } = props; + if (element.type === FourOhFourPage) { return {element}; } @@ -22,10 +23,14 @@ export default function layoutSelector({ element, props }) { if (/^\/guide(\/.*)*/.test(pathname)) { console.log('Hitting guide for some reason. Need a redirect.'); } - if ( - /^\/learn(\/.*)*/.test(pathname) && - false === /^\/learn\/$|^\/learn$/.test(pathname) - ) { + + const splitPath = pathname.split('/'); + const splitPathThree = splitPath.length > 2 ? splitPath[3] : ''; + + const isNotSuperBlockIntro = + splitPath.length > 3 && splitPathThree.length > 1; + + if (/^\/learn(\/.*)*/.test(pathname) && isNotSuperBlockIntro) { return ( {element} diff --git a/cypress/integration/learn/index.js b/cypress/integration/learn/index.js index ad0bae6275..8f121915d9 100644 --- a/cypress/integration/learn/index.js +++ b/cypress/integration/learn/index.js @@ -41,7 +41,7 @@ describe('Learn Landing page (not logged in)', () => { it('Should render a curriculum map', () => { cy.document().then(document => { const superBlocks = document.querySelectorAll( - `${selectors.challengeMap} > ul > li` + `${selectors.challengeMap} > li > a` ); expect(superBlocks).to.have.length(11); @@ -77,44 +77,9 @@ describe('Superblocks and Blocks', () => { cy.contains("Get started (it's free)").click(); }); - it('Has first superblock and block collapsed by default', () => { - 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)', () => { + it('Has all supeblocks visible', () => { cy.wrap(superBlockNames.slice(1)).each(name => { - cy.contains(name) - .should('be.visible') - .and('have.attr', 'aria-expanded', 'false'); + cy.contains(name).should('be.visible'); }); }); - 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'); - }); }); diff --git a/package-lock.json b/package-lock.json index 08bc3837d6..36f9de5a22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9480,6 +9480,12 @@ "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": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", diff --git a/package.json b/package.json index aa8ba1c54b..5a44102a5d 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "gray-matter": "^4.0.2", "husky": "^3.1.0", "jest": "^24.9.0", - "js-yaml": "^3.14.1", + "jest-dom": "^4.0.0", + "js-yaml": "^3.14.0", "lerna": "^3.22.1", "lint-staged": "^8.2.1", "lodash": "^4.17.20",