feat(Nav): Split Nav Bar in to Components

This commit is contained in:
Stuart Taylor
2018-03-12 13:28:40 +00:00
parent 410bda2394
commit d9921171f5
7 changed files with 290 additions and 195 deletions

View File

@ -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 (
<NavDropdown
id={ `nav-${content}-dropdown` }
key={ content }
noCaret={ true }
onClick={ openDropdown }
onMouseEnter={ openDropdown }
onMouseLeave={ closeDropdown }
onToggle={ isDropdownOpen ? closeDropdown : openDropdown }
open={ isDropdownOpen }
title={ content }
>
{ links.map(this.renderLink.bind(this, false)) }
</NavDropdown>
);
}
if (isReact) {
return (
<Link
key={ content }
onClick={ this.props[`handle${content}Click`] }
to={ link }
>
<Component
target={ target || null }
>
{ content }
</Component>
</Link>
);
}
return (
<Component
href={ link }
key={ content }
onClick={ this.props[`handle${content}Click`] }
target={ target || null }
>
{ content }
</Component>
);
}
render() {
const {
panes,
isSignedIn,
clickOnLogo,
clickOnMap,
showLoading
} = this.props;
const shouldShowMapButton = panes.length === 0;
return (
<Navbar
className='nav-height'
id='navbar'
staticTop={ true }
>
<div className='nav-component-wrapper'>
<Navbar.Header>
<Navbar.Toggle children={ 'Menu' } />
<NavbarBrand>
<a
href='/challenges/current-challenge'
onClick={ clickOnLogo }
>
<img
alt='learn to code javascript at freeCodeCamp logo'
className='img-responsive nav-logo logo'
src={ fCClogo }
/>
<img
alt='learn to code javascript at freeCodeCamp logo'
className='img-responsive logo-glyph'
src={ fCCglyph }
/>
</a>
</NavbarBrand>
<FCCSearchBar
dropdown={ true }
placeholder='&#xf002; Search 8,000+ lessons, articles, and videos'
/>
</Navbar.Header>
<Navbar.Collapse>
<Nav
navbar={ true }
pullRight={ true }
>
{
panes.map(({ content, actionCreator }) => (
<BinButton
content={ content }
handleClick={ actionCreator }
key={ content }
/>
))
}
{ shouldShowMapButton ?
<NoPropsPassThrough>
<li>
<Link
onClick={ clickOnMap }
to={ onRouteCurrentChallenge() }
>
Map
</Link>
</li>
</NoPropsPassThrough> :
null
}
{
navLinks.map(
this.renderLink.bind(this, true)
)
}
<SignUp
showLoading={ showLoading }
showSignUp={ !isSignedIn }
/>
</Nav>
</Navbar.Collapse>
</div>
</Navbar>
);
}
function FCCNav(props) {
const {
panes,
clickOnLogo,
clickOnMap,
shouldShowMapButton
} = props;
return (
<Navbar
className='nav-height'
id='navbar'
staticTop={ true }
>
<div className='nav-component-wrapper'>
<Navbar.Header>
<Navbar.Toggle children={ 'Menu' } />
<NavLogo clickOnLogo={ clickOnLogo } />
<FCCSearchBar
dropdown={ true }
placeholder='&#xf002; Search 8,000+ lessons, articles, and videos'
/>
</Navbar.Header>
<BinButtons panes={ panes } />
<Navbar.Collapse>
<NavLinks
clickOnMap={ clickOnMap }
shouldShowMapButton={ shouldShowMapButton }
/>
</Navbar.Collapse>
</div>
</Navbar>
);
}
FCCNav.displayName = 'FCCNav';

View File

@ -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 (
<div id='bin-buttons'>
<Nav>
{
panes.map(({ content, actionCreator }) => (
<BinButton
content={ content }
handleClick={ actionCreator }
key={ content }
/>
))
}
</Nav>
</div>
);
}
BinButtons.displayName = 'BinButtons';
BinButtons.propTypes = propTypes;
export default BinButtons;

View File

@ -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 (
<NavDropdown
id={ `nav-${content}-dropdown` }
key={ content }
noCaret={ true }
onClick={ openDropdown }
onMouseEnter={ openDropdown }
onMouseLeave={ closeDropdown }
onToggle={ isDropdownOpen ? closeDropdown : openDropdown }
open={ isDropdownOpen }
title={ content }
>
{ links.map(this.renderLink.bind(this, false)) }
</NavDropdown>
);
}
if (isReact) {
return (
<Link
key={ content }
onClick={ this.props[`handle${content}Click`] }
to={ link }
>
<Component
target={ target || null }
>
{ content }
</Component>
</Link>
);
}
return (
<Component
href={ link }
key={ content }
onClick={ this.props[`handle${content}Click`] }
target={ target || null }
>
{ content }
</Component>
);
}
render() {
const {
shouldShowMapButton,
clickOnMap,
showLoading,
isSignedIn,
navLinks
} = this.props;
return (
<Nav id='nav-links' navbar={ true } pullRight={ true }>
{
shouldShowMapButton ?
<NoPropsPassThrough>
<li>
<Link
onClick={ clickOnMap }
to={ onRouteCurrentChallenge() }
>
Map
</Link>
</li>
</NoPropsPassThrough> :
null
}
{
navLinks.map(
this.renderLink.bind(this, true)
)
}
<SignUp
showLoading={ showLoading }
showSignUp={ !isSignedIn }
/>
</Nav>
);
}
}
NavLinks.displayName = 'NavLinks';
NavLinks.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(NavLinks);

View File

@ -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 (
<NavbarBrand>
<a
href='/challenges/current-challenge'
onClick={ clickOnLogo }
>
<img
alt='learn to code javascript at freeCodeCamp logo'
className='img-responsive nav-logo logo'
src={ fCClogo }
/>
<img
alt='learn to code javascript at freeCodeCamp logo'
className='img-responsive logo-glyph'
src={ fCCglyph }
/>
</a>
</NavbarBrand>
);
}
NavLogo.displayName = 'NavLogo';
NavLogo.propTypes = propTypes;
export default NavLogo;

View File

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

View File

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