Merge pull request #16890 from Bouncey/fix/binButtons

Feat(Nav): Responsive Nav with promonent Bin buttons
This commit is contained in:
Berkeley Martinez
2018-03-20 16:45:30 -07:00
committed by GitHub
15 changed files with 587 additions and 249 deletions

View File

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

View File

@ -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 (
<Media
query='(min-width: 956px)'
render={
() => (
<Row>
<Col className='nav-component' sm={ 4 } xs={ 6 }>
<Navbar.Header>
<NavLogo clickOnLogo={ clickOnLogo } />
<FCCSearchBar
dropdown={ true }
placeholder=
'&#xf002; Search 8,000+ lessons, articles, and videos'
/>
</Navbar.Header>
</Col>
<Col className='nav-component bins' sm={ 4 } xs={ 6 }>
<BinButtons panes={ panes } />
</Col>
<Col className='nav-component nav-links' sm={ 4 } xs={ 0 }>
<Navbar.Collapse>
<NavLinks
clickOnMap={ clickOnMap }
shouldShowMapButton={ shouldShowMapButton }
/>
</Navbar.Collapse>
</Col>
</Row>
)
}
/>
);
}
LargeNav.displayName = 'LargeNav';
LargeNav.propTypes = propTypes;
export default LargeNav;

View File

@ -0,0 +1,51 @@
import React from 'react';
import Media from 'react-media';
import { 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 (
<Media
query={{ maxWidth: 955, minWidth: 751 }}
>
{
matches => matches && typeof window !== 'undefined' && (
<div>
<Row>
<Navbar.Header className='medium-nav'>
<div className='nav-component header'>
<Navbar.Toggle />
<NavLogo clickOnLogo={ clickOnLogo } />
<FCCSearchBar
dropdown={ true }
placeholder=
'&#xf002; Search 8,000+ lessons, articles, and videos'
/>
</div>
<div className='nav-component bins'>
<BinButtons panes={ panes } />
</div>
</Navbar.Header>
</Row>
<Row className='collapse-row'>
<Navbar.Collapse>
<NavLinks
clickOnMap={ clickOnMap }
shouldShowMapButton={ shouldShowMapButton }
/>
</Navbar.Collapse>
</Row>
</div>
)
}
</Media>
);
}
MediumNav.displayName = 'MediumNav';
MediumNav.propTypes = propTypes;
export default MediumNav;

View File

@ -1,54 +1,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
} from 'react-bootstrap';
import { 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 LargeNav from './LargeNav.jsx';
import MediumNav from './MediumNav.jsx';
import SmallNav from './SmallNav.jsx';
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';
import propTypes from './navPropTypes';
const mapStateToProps = createSelector(
isSignedInSelector,
dropdownSelector,
signInLoadingSelector,
panesSelector,
(
isSignedIn,
isDropdownOpen,
showLoading,
panes,
) => {
panes => {
return {
panes: panes.map(({ name, type }) => {
return {
@ -56,20 +24,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 +39,10 @@ function mapDispatchToProps(dispatch) {
clickOnLogo: e => {
e.preventDefault();
return clickOnLogo();
},
closeDropdown: () => closeDropdown(),
openDropdown: () => openDropdown()
}
), dispatch);
}
},
dispatch
);
dispatchers.dispatch = dispatch;
return () => dispatchers;
}
@ -102,156 +62,39 @@ 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
};
const allNavs = [
LargeNav,
MediumNav,
SmallNav
];
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>
);
function FCCNav(props) {
const {
panes,
clickOnLogo,
clickOnMap,
shouldShowMapButton
} = props;
const withNavProps = Component => (
<Component
clickOnLogo={ clickOnLogo }
clickOnMap={ clickOnMap }
key={ Component.displayName }
panes={ panes }
shouldShowMapButton={ shouldShowMapButton }
/>
);
return (
<Navbar
className='nav-height'
id='navbar'
staticTop={ true }
>
{
allNavs.map(withNavProps)
}
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>
);
}
</Navbar>
);
}
FCCNav.displayName = 'FCCNav';

View File

@ -0,0 +1,52 @@
import React from 'react';
import Media from 'react-media';
import { 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 (
<Media
query='(max-width: 750px)'
>
{
matches => matches && typeof window !== 'undefined' && (
<div>
<Row>
<Navbar.Header className='small-nav'>
<div className='nav-component header'>
<Navbar.Toggle />
<NavLogo clickOnLogo={ clickOnLogo } />
</div>
<div className='nav-component bins'>
<BinButtons panes={ panes } />
</div>
</Navbar.Header>
</Row>
<Row className='collapse-row'>
<Navbar.Collapse>
<NavLinks
clickOnMap={ clickOnMap }
shouldShowMapButton={ shouldShowMapButton }
>
<FCCSearchBar
dropdown={ true }
placeholder=
'&#xf002; Search 8,000+ lessons, articles, and videos'
/>
</NavLinks>
</Navbar.Collapse>
</Row>
</div>
)
}
</Media>
);
}
SmallNav.displayName = 'SmallNav';
SmallNav.propTypes = propTypes;
export default SmallNav;

View File

@ -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 (
<NavItem
<Button
bsStyle='primary'
onClick={ handleClick }
>
{ content }
</NavItem>
</Button>
);
}
BinButton.displayName = 'BinButton';

View File

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup } 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 (
<ButtonGroup>
{
panes.map(({ content, actionCreator }) => (
<BinButton
content={ content }
handleClick={ actionCreator }
key={ content }
/>
))
}
</ButtonGroup>
);
}
BinButtons.displayName = 'BinButtons';
BinButtons.propTypes = propTypes;
export default BinButtons;

View File

@ -0,0 +1,171 @@
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 = {
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,
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 }
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,
isInNav = true,
children
} = this.props;
return (
<Nav id='nav-links' navbar={ true } pullRight={ true }>
{ children }
{
shouldShowMapButton ?
<NoPropsPassThrough>
<li>
<Link
onClick={ clickOnMap }
to={ onRouteCurrentChallenge() }
>
Map
</Link>
</li>
</NoPropsPassThrough> :
null
}
{
navLinks.map(
this.renderLink.bind(this, isInNav)
)
}
<SignUp
isInDropDown={ !isInNav }
showLoading={ showLoading }
showSignUp={ !isSignedIn }
/>
</Nav>
);
}
}
NavLinks.displayName = 'NavLinks';
NavLinks.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(NavLinks);

View File

@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavbarBrand } from 'react-bootstrap';
import Media from 'react-media';
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 }
>
<Media query='(min-width: 735px)'>
{
matches => matches ? (
<img
alt='learn to code javascript at freeCodeCamp logo'
className='nav-logo logo'
src={ fCClogo }
/>
) : (
<img
alt='learn to code javascript at freeCodeCamp logo'
className='nav-logo logo'
src={ fCCglyph }
/>
)
}
</Media>
</a>
</NavbarBrand>
);
}
NavLogo.displayName = 'NavLogo';
NavLogo.propTypes = propTypes;
export default NavLogo;

View File

@ -1,21 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavItem } from 'react-bootstrap';
import { MenuItem, 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 = {
isInDropDown: PropTypes.bool,
showLoading: PropTypes.bool,
showSignUp: PropTypes.bool
};
export default function SignUpButton({ showLoading, showSignUp }) {
function SignUpButton({ isInDropDown, showLoading, showSignUp }) {
if (showLoading) {
return null;
}
if (showSignUp) {
return (
return isInDropDown ? (
<MenuItem
href='/signup'
key='signup'
>
Sign Up
</MenuItem>
) : (
<NavItem
href='/signup'
key='signup'
@ -38,3 +46,5 @@ export default function SignUpButton({ showLoading, showSignUp }) {
SignUpButton.displayName = 'SignUpButton';
SignUpButton.propTypes = propTypes;
export default SignUpButton;

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

View File

@ -223,64 +223,111 @@ li.nav-avatar {
}
}
.nav-component-wrapper {
.nav-component {
display: flex;
justify-content: space-evenly;
align-items: center;
&.header{
.navbar-brand {
padding-left: 0px;
}
}
&.bins {
justify-content: center;
.nav {
display: flex;
}
}
&.nav-links {
justify-content: end;
}
.fcc_searchBar {
width: auto;
flex-grow: 1
}
::-webkit-input-placeholder {
color: @input-color-placeholder;
}
::-moz-placeholder {
color: @input-color-placeholder;
}
::-ms-placeholder {
color: @input-color-placeholder;
}
::placeholder {
color: @input-color-placeholder;
}
.navbar-header {
flex-grow: 1;
::-webkit-input-placeholder {
color: @input-color-placeholder;
}
::-moz-placeholder {
color: @input-color-placeholder;
}
::-ms-placeholder {
color: @input-color-placeholder;
}
::placeholder {
color: @input-color-placeholder;
}
}
.navbar-header {
display: flex;
align-items: center;
margin-right: 10px;
width: 100%;
}
}
.logo-glyph {
height: 28px;
width: auto;
}
.medium-nav {
display: flex;
justify-content: space-between;
.logo {
display: none !important;
}
.bins {
justify-content: flex-end;
@media (min-width: 992px) {
.logo-glyph {
display: none !important;
}
}
.small-nav {
display: flex;
justify-content: space-between;
.bins {
justify-content: flex-end;
.btn {
padding: 6px 4px;
}
}
}
.bins {
.btn {
border-color: @brand-primary;
background-color: white;
&:hover {
background-color: @brand-primary;
}
.logo {
display: block !important;
}
}
@media screen and (max-width: 768px) {
.nav-component-wrapper {
display: block
.collapse-row {
background-color: @brand-primary;
.dropdown-menu {
li a {
display: flex;
justify-content: center;
background-color: #eeeeee;
color: @brand-primary !important;
&:hover {
background-color: @brand-primary !important;
color: #eeeeee !important;
}
}
}
}

View File

@ -0,0 +1,8 @@
import PropTypes from 'prop-types';
export default {
clickOnLogo: PropTypes.func.isRequired,
clickOnMap: PropTypes.func.isRequired,
panes: PropTypes.array,
shouldShowMapButton: PropTypes.bool
};

23
package-lock.json generated
View File

@ -9244,6 +9244,14 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=",
"requires": {
"string-convert": "0.2.1"
}
},
"json3": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@ -14128,6 +14136,16 @@
"deep-equal": "1.0.1"
}
},
"react-media": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/react-media/-/react-media-1.8.0.tgz",
"integrity": "sha512-XcfqkDQj5/hmJod/kXUAZljJyMVkWrBWOkzwynAR8BXOGlbFLGBwezM0jQHtp2BrSymhf14/XrQrb3gGBnGK4g==",
"requires": {
"invariant": "2.2.2",
"json2mq": "0.2.0",
"prop-types": "15.6.0"
}
},
"react-motion": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.4.8.tgz",
@ -16361,6 +16379,11 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c="
},
"string-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz",

View File

@ -121,6 +121,7 @@
"react-freecodecamp-search": "^1.4.1",
"react-helmet": "^5.2.0",
"react-images": "^0.5.1",
"react-media": "^1.8.0",
"react-motion": "~0.4.2",
"react-no-ssr": "^1.0.1",
"react-notification": "git+https://github.com/BerkeleyTrue/react-notification.git#freecodecamp",