Merge pull request #16890 from Bouncey/fix/binButtons
Feat(Nav): Responsive Nav with promonent Bin buttons
This commit is contained in:
@ -328,7 +328,7 @@
|
|||||||
@grid-gutter-width: 30px;
|
@grid-gutter-width: 30px;
|
||||||
// Navbar collapse
|
// Navbar collapse
|
||||||
//** Point at which the navbar becomes uncollapsed.
|
//** Point at which the navbar becomes uncollapsed.
|
||||||
@grid-float-breakpoint: @screen-sm-min;
|
@grid-float-breakpoint: 955px;
|
||||||
//** Point at which the navbar begins collapsing.
|
//** Point at which the navbar begins collapsing.
|
||||||
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
|
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
|
||||||
|
|
||||||
|
47
common/app/Nav/LargeNav.jsx
Normal file
47
common/app/Nav/LargeNav.jsx
Normal 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=
|
||||||
|
' 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;
|
51
common/app/Nav/MediumNav.jsx
Normal file
51
common/app/Nav/MediumNav.jsx
Normal 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=
|
||||||
|
' 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;
|
@ -1,54 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import capitalize from 'lodash/capitalize';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import FCCSearchBar from 'react-freecodecamp-search';
|
import { Navbar } from 'react-bootstrap';
|
||||||
import {
|
|
||||||
MenuItem,
|
|
||||||
Nav,
|
|
||||||
NavDropdown,
|
|
||||||
NavItem,
|
|
||||||
Navbar,
|
|
||||||
NavbarBrand
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
|
|
||||||
import NoPropsPassThrough from '../utils/No-Props-Passthrough.jsx';
|
import LargeNav from './LargeNav.jsx';
|
||||||
import { Link } from '../Router';
|
import MediumNav from './MediumNav.jsx';
|
||||||
import navLinks from './links.json';
|
import SmallNav from './SmallNav.jsx';
|
||||||
import SignUp from './Sign-Up.jsx';
|
|
||||||
import BinButton from './Bin-Button.jsx';
|
|
||||||
import {
|
import {
|
||||||
clickOnLogo,
|
clickOnLogo,
|
||||||
clickOnMap,
|
clickOnMap
|
||||||
openDropdown,
|
|
||||||
closeDropdown,
|
|
||||||
createNavLinkActionCreator,
|
|
||||||
|
|
||||||
dropdownSelector
|
|
||||||
} from './redux';
|
} from './redux';
|
||||||
import { isSignedInSelector, signInLoadingSelector } from '../redux';
|
|
||||||
import { panesSelector } from '../Panes/redux';
|
import { panesSelector } from '../Panes/redux';
|
||||||
import { onRouteCurrentChallenge } from '../routes/Challenges/redux';
|
import propTypes from './navPropTypes';
|
||||||
|
|
||||||
|
|
||||||
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(
|
const mapStateToProps = createSelector(
|
||||||
isSignedInSelector,
|
|
||||||
dropdownSelector,
|
|
||||||
signInLoadingSelector,
|
|
||||||
panesSelector,
|
panesSelector,
|
||||||
(
|
panes => {
|
||||||
isSignedIn,
|
|
||||||
isDropdownOpen,
|
|
||||||
showLoading,
|
|
||||||
panes,
|
|
||||||
) => {
|
|
||||||
return {
|
return {
|
||||||
panes: panes.map(({ name, type }) => {
|
panes: panes.map(({ name, type }) => {
|
||||||
return {
|
return {
|
||||||
@ -56,20 +24,13 @@ const mapStateToProps = createSelector(
|
|||||||
action: type
|
action: type
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
isDropdownOpen,
|
shouldShowMapButton: panes.length === 0
|
||||||
isSignedIn,
|
|
||||||
showLoading
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
const dispatchers = bindActionCreators(navLinks.reduce(
|
const dispatchers = bindActionCreators(
|
||||||
(mdtp, { content }) => {
|
|
||||||
const handler = `handle${capitalize(content)}Click`;
|
|
||||||
mdtp[handler] = createNavLinkActionCreator(content);
|
|
||||||
return mdtp;
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
clickOnMap: e => {
|
clickOnMap: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -78,11 +39,10 @@ function mapDispatchToProps(dispatch) {
|
|||||||
clickOnLogo: e => {
|
clickOnLogo: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return clickOnLogo();
|
return clickOnLogo();
|
||||||
},
|
}
|
||||||
closeDropdown: () => closeDropdown(),
|
},
|
||||||
openDropdown: () => openDropdown()
|
dispatch
|
||||||
}
|
);
|
||||||
), dispatch);
|
|
||||||
dispatchers.dispatch = dispatch;
|
dispatchers.dispatch = dispatch;
|
||||||
return () => dispatchers;
|
return () => dispatchers;
|
||||||
}
|
}
|
||||||
@ -102,156 +62,39 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const propTypes = {
|
const allNavs = [
|
||||||
clickOnLogo: PropTypes.func.isRequired,
|
LargeNav,
|
||||||
clickOnMap: PropTypes.func.isRequired,
|
MediumNav,
|
||||||
closeDropdown: PropTypes.func.isRequired,
|
SmallNav
|
||||||
isDropdownOpen: PropTypes.bool,
|
];
|
||||||
isSignedIn: PropTypes.bool,
|
|
||||||
openDropdown: PropTypes.func.isRequired,
|
|
||||||
panes: PropTypes.array,
|
|
||||||
showLoading: PropTypes.bool,
|
|
||||||
signedIn: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export class FCCNav extends React.Component {
|
function FCCNav(props) {
|
||||||
renderLink(isNavItem, { isReact, isDropdown, content, link, links, target }) {
|
const {
|
||||||
const Component = isNavItem ? NavItem : MenuItem;
|
panes,
|
||||||
const {
|
clickOnLogo,
|
||||||
isDropdownOpen,
|
clickOnMap,
|
||||||
openDropdown,
|
shouldShowMapButton
|
||||||
closeDropdown
|
} = props;
|
||||||
} = this.props;
|
const withNavProps = Component => (
|
||||||
|
<Component
|
||||||
if (isDropdown) {
|
clickOnLogo={ clickOnLogo }
|
||||||
// adding a noop to NavDropdown to disable false warning
|
clickOnMap={ clickOnMap }
|
||||||
// about controlled component
|
key={ Component.displayName }
|
||||||
return (
|
panes={ panes }
|
||||||
<NavDropdown
|
shouldShowMapButton={ shouldShowMapButton }
|
||||||
id={ `nav-${content}-dropdown` }
|
/>
|
||||||
key={ content }
|
);
|
||||||
noCaret={ true }
|
return (
|
||||||
onClick={ openDropdown }
|
<Navbar
|
||||||
onMouseEnter={ openDropdown }
|
className='nav-height'
|
||||||
onMouseLeave={ closeDropdown }
|
id='navbar'
|
||||||
onToggle={ isDropdownOpen ? closeDropdown : openDropdown }
|
staticTop={ true }
|
||||||
open={ isDropdownOpen }
|
>
|
||||||
title={ content }
|
{
|
||||||
>
|
allNavs.map(withNavProps)
|
||||||
{ links.map(this.renderLink.bind(this, false)) }
|
|
||||||
</NavDropdown>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isReact) {
|
</Navbar>
|
||||||
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=' 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FCCNav.displayName = 'FCCNav';
|
FCCNav.displayName = 'FCCNav';
|
||||||
|
52
common/app/Nav/SmallNav.jsx
Normal file
52
common/app/Nav/SmallNav.jsx
Normal 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=
|
||||||
|
' Search 8,000+ lessons, articles, and videos'
|
||||||
|
/>
|
||||||
|
</NavLinks>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Media>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SmallNav.displayName = 'SmallNav';
|
||||||
|
SmallNav.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default SmallNav;
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NavItem } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
content: PropTypes.string,
|
content: PropTypes.string,
|
||||||
@ -9,11 +9,12 @@ const propTypes = {
|
|||||||
|
|
||||||
export default function BinButton({ content, handleClick }) {
|
export default function BinButton({ content, handleClick }) {
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<Button
|
||||||
|
bsStyle='primary'
|
||||||
onClick={ handleClick }
|
onClick={ handleClick }
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
</NavItem>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
BinButton.displayName = 'BinButton';
|
BinButton.displayName = 'BinButton';
|
34
common/app/Nav/components/BinButtons.jsx
Normal file
34
common/app/Nav/components/BinButtons.jsx
Normal 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;
|
171
common/app/Nav/components/NavLinks.jsx
Normal file
171
common/app/Nav/components/NavLinks.jsx
Normal 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);
|
47
common/app/Nav/components/NavLogo.jsx
Normal file
47
common/app/Nav/components/NavLogo.jsx
Normal 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;
|
@ -1,21 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { NavItem } from 'react-bootstrap';
|
import { MenuItem, NavItem } from 'react-bootstrap';
|
||||||
|
|
||||||
import { Link } from '../Router';
|
import { Link } from '../../Router';
|
||||||
import { onRouteSettings } from '../routes/Settings/redux';
|
import { onRouteSettings } from '../../routes/Settings/redux';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
isInDropDown: PropTypes.bool,
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
showSignUp: PropTypes.bool
|
showSignUp: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SignUpButton({ showLoading, showSignUp }) {
|
function SignUpButton({ isInDropDown, showLoading, showSignUp }) {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (showSignUp) {
|
if (showSignUp) {
|
||||||
return (
|
return isInDropDown ? (
|
||||||
|
<MenuItem
|
||||||
|
href='/signup'
|
||||||
|
key='signup'
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
<NavItem
|
<NavItem
|
||||||
href='/signup'
|
href='/signup'
|
||||||
key='signup'
|
key='signup'
|
||||||
@ -38,3 +46,5 @@ export default function SignUpButton({ showLoading, showSignUp }) {
|
|||||||
|
|
||||||
SignUpButton.displayName = 'SignUpButton';
|
SignUpButton.displayName = 'SignUpButton';
|
||||||
SignUpButton.propTypes = propTypes;
|
SignUpButton.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default SignUpButton;
|
3
common/app/Nav/components/index.js
Normal file
3
common/app/Nav/components/index.js
Normal 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';
|
@ -223,64 +223,111 @@ li.nav-avatar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-component-wrapper {
|
.nav-component {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&.header{
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bins {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&.nav-links {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
.fcc_searchBar {
|
.fcc_searchBar {
|
||||||
width: auto;
|
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;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 10px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-glyph {
|
.medium-nav {
|
||||||
height: 28px;
|
display: flex;
|
||||||
width: auto;
|
justify-content: space-between;
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.bins {
|
||||||
display: none !important;
|
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) {
|
.collapse-row {
|
||||||
.nav-component-wrapper {
|
background-color: @brand-primary;
|
||||||
display: block
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
8
common/app/Nav/navPropTypes.js
Normal file
8
common/app/Nav/navPropTypes.js
Normal 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
23
package-lock.json
generated
@ -9244,6 +9244,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
"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": {
|
"json3": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
|
||||||
@ -14128,6 +14136,16 @@
|
|||||||
"deep-equal": "1.0.1"
|
"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": {
|
"react-motion": {
|
||||||
"version": "0.4.8",
|
"version": "0.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.4.8.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
"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": {
|
"string-length": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz",
|
||||||
|
@ -121,6 +121,7 @@
|
|||||||
"react-freecodecamp-search": "^1.4.1",
|
"react-freecodecamp-search": "^1.4.1",
|
||||||
"react-helmet": "^5.2.0",
|
"react-helmet": "^5.2.0",
|
||||||
"react-images": "^0.5.1",
|
"react-images": "^0.5.1",
|
||||||
|
"react-media": "^1.8.0",
|
||||||
"react-motion": "~0.4.2",
|
"react-motion": "~0.4.2",
|
||||||
"react-no-ssr": "^1.0.1",
|
"react-no-ssr": "^1.0.1",
|
||||||
"react-notification": "git+https://github.com/BerkeleyTrue/react-notification.git#freecodecamp",
|
"react-notification": "git+https://github.com/BerkeleyTrue/react-notification.git#freecodecamp",
|
||||||
|
Reference in New Issue
Block a user