From a81ec38dbdbed8d42a452e0d9d518fb73b1d4fe6 Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Mon, 12 Mar 2018 13:28:40 +0000 Subject: [PATCH 1/2] feat(Nav): Split Nav Bar in to Components --- common/app/Nav/Nav.jsx | 236 ++++-------------- .../app/Nav/{ => components}/Bin-Button.jsx | 0 common/app/Nav/components/BinButtons.jsx | 36 +++ common/app/Nav/components/NavLinks.jsx | 167 +++++++++++++ common/app/Nav/components/NavLogo.jsx | 39 +++ common/app/Nav/{ => components}/Sign-Up.jsx | 4 +- common/app/Nav/components/index.js | 3 + 7 files changed, 290 insertions(+), 195 deletions(-) rename common/app/Nav/{ => components}/Bin-Button.jsx (100%) create mode 100644 common/app/Nav/components/BinButtons.jsx create mode 100644 common/app/Nav/components/NavLinks.jsx create mode 100644 common/app/Nav/components/NavLogo.jsx rename common/app/Nav/{ => components}/Sign-Up.jsx (87%) create mode 100644 common/app/Nav/components/index.js diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx index 463519a77e..2fb225beca 100644 --- a/common/app/Nav/Nav.jsx +++ b/common/app/Nav/Nav.jsx @@ -2,53 +2,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import capitalize from 'lodash/capitalize'; import { createSelector } from 'reselect'; import FCCSearchBar from 'react-freecodecamp-search'; import { - MenuItem, - Nav, - NavDropdown, - NavItem, - Navbar, - NavbarBrand + Navbar } from 'react-bootstrap'; -import NoPropsPassThrough from '../utils/No-Props-Passthrough.jsx'; -import { Link } from '../Router'; -import navLinks from './links.json'; -import SignUp from './Sign-Up.jsx'; -import BinButton from './Bin-Button.jsx'; +import { BinButtons, NavLogo, NavLinks } from './components'; import { clickOnLogo, - clickOnMap, - openDropdown, - closeDropdown, - createNavLinkActionCreator, - - dropdownSelector + clickOnMap } from './redux'; -import { isSignedInSelector, signInLoadingSelector } from '../redux'; import { panesSelector } from '../Panes/redux'; -import { onRouteCurrentChallenge } from '../routes/Challenges/redux'; - - -const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; -// TODO @freecodecamp-team: place this glyph in S3 like above, PR in /assets -const fCCglyph = 'https://raw.githubusercontent.com/freeCodeCamp/assets/' + - '3b9cafc312802199ebba8b31fb1ed9b466a3efbb/assets/logos/FFCFire.png'; const mapStateToProps = createSelector( - isSignedInSelector, - dropdownSelector, - signInLoadingSelector, panesSelector, - ( - isSignedIn, - isDropdownOpen, - showLoading, - panes, - ) => { + panes => { return { panes: panes.map(({ name, type }) => { return { @@ -56,20 +25,13 @@ const mapStateToProps = createSelector( action: type }; }, {}), - isDropdownOpen, - isSignedIn, - showLoading + shouldShowMapButton: panes.length === 0 }; } ); function mapDispatchToProps(dispatch) { - const dispatchers = bindActionCreators(navLinks.reduce( - (mdtp, { content }) => { - const handler = `handle${capitalize(content)}Click`; - mdtp[handler] = createNavLinkActionCreator(content); - return mdtp; - }, + const dispatchers = bindActionCreators( { clickOnMap: e => { e.preventDefault(); @@ -78,11 +40,10 @@ function mapDispatchToProps(dispatch) { clickOnLogo: e => { e.preventDefault(); return clickOnLogo(); - }, - closeDropdown: () => closeDropdown(), - openDropdown: () => openDropdown() - } - ), dispatch); + } + }, + dispatch + ); dispatchers.dispatch = dispatch; return () => dispatchers; } @@ -105,153 +66,42 @@ function mergeProps(stateProps, dispatchProps, ownProps) { const propTypes = { clickOnLogo: PropTypes.func.isRequired, clickOnMap: PropTypes.func.isRequired, - closeDropdown: PropTypes.func.isRequired, - isDropdownOpen: PropTypes.bool, - isSignedIn: PropTypes.bool, - openDropdown: PropTypes.func.isRequired, panes: PropTypes.array, - showLoading: PropTypes.bool, - signedIn: PropTypes.bool + shouldShowMapButton: PropTypes.bool }; -export class FCCNav extends React.Component { - renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) { - const Component = isNavItem ? NavItem : MenuItem; - const { - isDropdownOpen, - openDropdown, - closeDropdown - } = this.props; - - if (isDropdown) { - // adding a noop to NavDropdown to disable false warning - // about controlled component - return ( - - { links.map(this.renderLink.bind(this, false)) } - - ); - } - if (isReact) { - return ( - - - { content } - - - ); - } - return ( - - { content } - - ); - } - - render() { - const { - panes, - isSignedIn, - clickOnLogo, - clickOnMap, - showLoading - } = this.props; - - const shouldShowMapButton = panes.length === 0; - return ( - -
- - - - - learn to code javascript at freeCodeCamp logo - learn to code javascript at freeCodeCamp logo - - - - - - - -
-
- ); - } +function FCCNav(props) { + const { + panes, + clickOnLogo, + clickOnMap, + shouldShowMapButton + } = props; + return ( + +
+ + + + + + + + + +
+
+ ); } FCCNav.displayName = 'FCCNav'; diff --git a/common/app/Nav/Bin-Button.jsx b/common/app/Nav/components/Bin-Button.jsx similarity index 100% rename from common/app/Nav/Bin-Button.jsx rename to common/app/Nav/components/Bin-Button.jsx diff --git a/common/app/Nav/components/BinButtons.jsx b/common/app/Nav/components/BinButtons.jsx new file mode 100644 index 0000000000..67ae2072d6 --- /dev/null +++ b/common/app/Nav/components/BinButtons.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Nav } from 'react-bootstrap'; +import BinButton from './Bin-Button.jsx'; + +const propTypes = { + panes: PropTypes.arrayOf( + PropTypes.shape({ + actionCreator: PropTypes.func.isRequired, + content: PropTypes.string.isRequired + }) + ) +}; + +function BinButtons({ panes }) { + return ( +
+ +
+ ); +} + +BinButtons.displayName = 'BinButtons'; +BinButtons.propTypes = propTypes; + +export default BinButtons; diff --git a/common/app/Nav/components/NavLinks.jsx b/common/app/Nav/components/NavLinks.jsx new file mode 100644 index 0000000000..2a8cec2024 --- /dev/null +++ b/common/app/Nav/components/NavLinks.jsx @@ -0,0 +1,167 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { capitalize } from 'lodash'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { MenuItem, NavDropdown, NavItem, Nav } from 'react-bootstrap'; + +import navLinks from '../links.json'; +import SignUp from './Sign-Up.jsx'; +import NoPropsPassThrough from '../../utils/No-Props-Passthrough.jsx'; +import { Link } from '../../Router'; + +import { onRouteCurrentChallenge } from '../../routes/Challenges/redux'; +import { + openDropdown, + closeDropdown, + dropdownSelector, + createNavLinkActionCreator +} from '../redux'; +import { isSignedInSelector, signInLoadingSelector } from '../../redux'; + +const mapStateToProps = createSelector( + isSignedInSelector, + dropdownSelector, + signInLoadingSelector, + (isSignedIn, isDropdownOpen, showLoading) => ({ + isDropdownOpen, + isSignedIn, + navLinks, + showLoading + }) +); + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + { + ...navLinks.reduce( + (mdtp, { content }) => { + const handler = `handle${capitalize(content)}Click`; + mdtp[handler] = createNavLinkActionCreator(content); + return mdtp; + }), + closeDropdown, + openDropdown + }, + dispatch + ); +} + +const navLinkPropType = PropTypes.shape({ + content: PropTypes.string, + link: PropTypes.string, + isDropdown: PropTypes.bool, + target: PropTypes.string, + links: PropTypes.array +}); + +const propTypes = { + clickOnMap: PropTypes.func.isRequired, + closeDropdown: PropTypes.func.isRequired, + isDropdownOpen: PropTypes.bool, + isSignedIn: PropTypes.bool, + navLinks: PropTypes.arrayOf(navLinkPropType), + openDropdown: PropTypes.func.isRequired, + shouldShowMapButton: PropTypes.bool, + showLoading: PropTypes.bool +}; + +class NavLinks extends PureComponent { + + renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) { + const Component = isNavItem ? NavItem : MenuItem; + const { + isDropdownOpen, + openDropdown, + closeDropdown + } = this.props; + + if (isDropdown) { + // adding a noop to NavDropdown to disable false warning + // about controlled component + return ( + + { links.map(this.renderLink.bind(this, false)) } + + ); + } + if (isReact) { + return ( + + + { content } + + + ); + } + return ( + + { content } + + ); + } + + render() { + const { + shouldShowMapButton, + clickOnMap, + showLoading, + isSignedIn, + navLinks + } = this.props; + return ( + + ); + } +} + +NavLinks.displayName = 'NavLinks'; +NavLinks.propTypes = propTypes; + +export default connect(mapStateToProps, mapDispatchToProps)(NavLinks); diff --git a/common/app/Nav/components/NavLogo.jsx b/common/app/Nav/components/NavLogo.jsx new file mode 100644 index 0000000000..9f06918d61 --- /dev/null +++ b/common/app/Nav/components/NavLogo.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { NavbarBrand } from 'react-bootstrap'; + +const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg'; +// TODO @freecodecamp-team: place this glyph in S3 like above, PR in /assets +const fCCglyph = 'https://raw.githubusercontent.com/freeCodeCamp/assets/' + + '3b9cafc312802199ebba8b31fb1ed9b466a3efbb/assets/logos/FFCFire.png'; + +const propTypes = { + clickOnLogo: PropTypes.func.isRequired +}; + +function NavLogo({ clickOnLogo }) { + return ( + + + learn to code javascript at freeCodeCamp logo + learn to code javascript at freeCodeCamp logo + + + ); +} + +NavLogo.displayName = 'NavLogo'; +NavLogo.propTypes = propTypes; + +export default NavLogo; diff --git a/common/app/Nav/Sign-Up.jsx b/common/app/Nav/components/Sign-Up.jsx similarity index 87% rename from common/app/Nav/Sign-Up.jsx rename to common/app/Nav/components/Sign-Up.jsx index b91c9e895a..922ba52eca 100644 --- a/common/app/Nav/Sign-Up.jsx +++ b/common/app/Nav/components/Sign-Up.jsx @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { NavItem } from 'react-bootstrap'; -import { Link } from '../Router'; -import { onRouteSettings } from '../routes/Settings/redux'; +import { Link } from '../../Router'; +import { onRouteSettings } from '../../routes/Settings/redux'; const propTypes = { showLoading: PropTypes.bool, diff --git a/common/app/Nav/components/index.js b/common/app/Nav/components/index.js new file mode 100644 index 0000000000..a406b1ec16 --- /dev/null +++ b/common/app/Nav/components/index.js @@ -0,0 +1,3 @@ +export { default as BinButtons } from './BinButtons.jsx'; +export { default as NavLogo } from './NavLogo.jsx'; +export { default as NavLinks } from './NavLinks.jsx'; From 33d0d05adbe478a5847f46665baa51a67c41435b Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Tue, 13 Mar 2018 20:10:06 +0000 Subject: [PATCH 2/2] feat(nav): Use screen width specific navs --- client/less/lib/bootstrap/variables.less | 2 +- common/app/Nav/LargeNav.jsx | 47 ++++++++++ common/app/Nav/MediumNav.jsx | 52 +++++++++++ common/app/Nav/Nav.jsx | 53 +++++------ common/app/Nav/SmallNav.jsx | 53 +++++++++++ common/app/Nav/components/Bin-Button.jsx | 7 +- common/app/Nav/components/BinButtons.jsx | 26 +++--- common/app/Nav/components/NavLinks.jsx | 12 ++- common/app/Nav/components/NavLogo.jsx | 30 +++--- common/app/Nav/components/Sign-Up.jsx | 16 +++- common/app/Nav/nav.less | 112 ++++++++++++++--------- common/app/Nav/navPropTypes.js | 8 ++ package-lock.json | 23 +++++ package.json | 1 + 14 files changed, 333 insertions(+), 109 deletions(-) create mode 100644 common/app/Nav/LargeNav.jsx create mode 100644 common/app/Nav/MediumNav.jsx create mode 100644 common/app/Nav/SmallNav.jsx create mode 100644 common/app/Nav/navPropTypes.js diff --git a/client/less/lib/bootstrap/variables.less b/client/less/lib/bootstrap/variables.less index 019b8e7a58..15fcda30c3 100755 --- a/client/less/lib/bootstrap/variables.less +++ b/client/less/lib/bootstrap/variables.less @@ -328,7 +328,7 @@ @grid-gutter-width: 30px; // Navbar collapse //** Point at which the navbar becomes uncollapsed. -@grid-float-breakpoint: @screen-sm-min; +@grid-float-breakpoint: 955px; //** Point at which the navbar begins collapsing. @grid-float-breakpoint-max: (@grid-float-breakpoint - 1); diff --git a/common/app/Nav/LargeNav.jsx b/common/app/Nav/LargeNav.jsx new file mode 100644 index 0000000000..e9d4052a6c --- /dev/null +++ b/common/app/Nav/LargeNav.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Media from 'react-media'; +import { Col, Navbar, Row } from 'react-bootstrap'; +import FCCSearchBar from 'react-freecodecamp-search'; +import { NavLogo, BinButtons, NavLinks } from './components'; + +import propTypes from './navPropTypes'; + +function LargeNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) { + return ( + ( + + + + + + + + + + + + + + + + + ) + } + /> + ); +} + +LargeNav.displayName = 'LargeNav'; +LargeNav.propTypes = propTypes; + +export default LargeNav; diff --git a/common/app/Nav/MediumNav.jsx b/common/app/Nav/MediumNav.jsx new file mode 100644 index 0000000000..498b7b367e --- /dev/null +++ b/common/app/Nav/MediumNav.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import Media from 'react-media'; +import { Col, Navbar, Row } from 'react-bootstrap'; +import FCCSearchBar from 'react-freecodecamp-search'; +import { NavLogo, BinButtons, NavLinks } from './components'; + +import propTypes from './navPropTypes'; + +function MediumNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) { + return ( + ( +
+ + +
+ + + +
+
+ +
+
+
+ + + + + + + +
+ ) + } + /> + ); +} + +MediumNav.displayName = 'MediumNav'; +MediumNav.propTypes = propTypes; + +export default MediumNav; diff --git a/common/app/Nav/Nav.jsx b/common/app/Nav/Nav.jsx index 2fb225beca..1e41d1b155 100644 --- a/common/app/Nav/Nav.jsx +++ b/common/app/Nav/Nav.jsx @@ -1,19 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import FCCSearchBar from 'react-freecodecamp-search'; -import { - Navbar -} from 'react-bootstrap'; +import { Navbar } from 'react-bootstrap'; -import { BinButtons, NavLogo, NavLinks } from './components'; +import LargeNav from './LargeNav.jsx'; +import MediumNav from './MediumNav.jsx'; +import SmallNav from './SmallNav.jsx'; import { clickOnLogo, clickOnMap } from './redux'; import { panesSelector } from '../Panes/redux'; +import propTypes from './navPropTypes'; const mapStateToProps = createSelector( panesSelector, @@ -63,13 +62,6 @@ function mergeProps(stateProps, dispatchProps, ownProps) { }; } -const propTypes = { - clickOnLogo: PropTypes.func.isRequired, - clickOnMap: PropTypes.func.isRequired, - panes: PropTypes.array, - shouldShowMapButton: PropTypes.bool -}; - function FCCNav(props) { const { panes, @@ -83,23 +75,24 @@ function FCCNav(props) { id='navbar' staticTop={ true } > -
- - - - - - - - - -
+ + + ); } diff --git a/common/app/Nav/SmallNav.jsx b/common/app/Nav/SmallNav.jsx new file mode 100644 index 0000000000..16713ea238 --- /dev/null +++ b/common/app/Nav/SmallNav.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import Media from 'react-media'; +import { Col, Navbar, Row } from 'react-bootstrap'; +import FCCSearchBar from 'react-freecodecamp-search'; +import { NavLogo, BinButtons, NavLinks } from './components'; + +import propTypes from './navPropTypes'; + +function SmallNav({ clickOnLogo, clickOnMap, shouldShowMapButton, panes }) { + return ( + ( +
+ + +
+ + +
+
+ +
+
+
+ + + + + + + + + +
+ ) + } + /> + ); +} + +SmallNav.displayName = 'SmallNav'; +SmallNav.propTypes = propTypes; + +export default SmallNav; diff --git a/common/app/Nav/components/Bin-Button.jsx b/common/app/Nav/components/Bin-Button.jsx index 21360d4cdf..d0ec1f35f5 100644 --- a/common/app/Nav/components/Bin-Button.jsx +++ b/common/app/Nav/components/Bin-Button.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { NavItem } from 'react-bootstrap'; +import { Button } from 'react-bootstrap'; const propTypes = { content: PropTypes.string, @@ -9,11 +9,12 @@ const propTypes = { export default function BinButton({ content, handleClick }) { return ( - { content } - + ); } BinButton.displayName = 'BinButton'; diff --git a/common/app/Nav/components/BinButtons.jsx b/common/app/Nav/components/BinButtons.jsx index 67ae2072d6..f6d429f4e1 100644 --- a/common/app/Nav/components/BinButtons.jsx +++ b/common/app/Nav/components/BinButtons.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Nav } from 'react-bootstrap'; +import { ButtonGroup } from 'react-bootstrap'; import BinButton from './Bin-Button.jsx'; const propTypes = { @@ -14,19 +14,17 @@ const propTypes = { function BinButtons({ panes }) { return ( -
- -
+ + { + panes.map(({ content, actionCreator }) => ( + + )) + } + ); } diff --git a/common/app/Nav/components/NavLinks.jsx b/common/app/Nav/components/NavLinks.jsx index 2a8cec2024..02a33cdc9f 100644 --- a/common/app/Nav/components/NavLinks.jsx +++ b/common/app/Nav/components/NavLinks.jsx @@ -57,9 +57,11 @@ const navLinkPropType = PropTypes.shape({ }); const propTypes = { + children: PropTypes.any, clickOnMap: PropTypes.func.isRequired, closeDropdown: PropTypes.func.isRequired, isDropdownOpen: PropTypes.bool, + isInNav: PropTypes.bool, isSignedIn: PropTypes.bool, navLinks: PropTypes.arrayOf(navLinkPropType), openDropdown: PropTypes.func.isRequired, @@ -86,8 +88,6 @@ class NavLinks extends PureComponent { key={ content } noCaret={ true } onClick={ openDropdown } - onMouseEnter={ openDropdown } - onMouseLeave={ closeDropdown } onToggle={ isDropdownOpen ? closeDropdown : openDropdown } open={ isDropdownOpen } title={ content } @@ -129,10 +129,13 @@ class NavLinks extends PureComponent { clickOnMap, showLoading, isSignedIn, - navLinks + navLinks, + isInNav = true, + children } = this.props; return (