diff --git a/client/gatsby-browser.js b/client/gatsby-browser.js index b66417a193..9c69ef5b9f 100644 --- a/client/gatsby-browser.js +++ b/client/gatsby-browser.js @@ -10,6 +10,7 @@ import { DefaultLayout, GuideLayout } from './src/components/layouts'; +import GuideNavMenu from './src/components/layouts/components/guide/NavMenu'; const store = createStore(); @@ -41,7 +42,7 @@ export const wrapPageElement = ({ element, props }) => { } if (/^\/guide(\/.*)*/.test(pathname)) { return ( - + }> {element} ); diff --git a/client/gatsby-ssr.js b/client/gatsby-ssr.js index d369a5c515..312207d60e 100644 --- a/client/gatsby-ssr.js +++ b/client/gatsby-ssr.js @@ -5,12 +5,9 @@ import { Provider } from 'react-redux'; import headComponents from './src/head'; import { createStore } from './src/redux/createStore'; +import { wrapPageElement } from './gatsby-browser'; -import { - CertificationLayout, - DefaultLayout, - GuideLayout -} from './src/components/layouts'; +export { wrapPageElement }; const store = createStore(); @@ -22,39 +19,6 @@ wrapRootElement.propTypes = { element: PropTypes.any }; -export const wrapPageElement = ({ element, props }) => { - const { - location: { pathname } - } = props; - if (pathname === '/') { - return ( - - {element} - - ); - } - if (/^\/certification(\/.*)*/.test(pathname)) { - return {element}; - } - if (/^\/guide(\/.*)*/.test(pathname)) { - return ( - - {element} - - ); - } - if (/^\/learn(\/.*)*/.test(pathname)) { - return {element}; - } - return {element}; -}; - -wrapPageElement.propTypes = { - element: PropTypes.any, - location: PropTypes.objectOf({ pathname: PropTypes.string }), - props: PropTypes.any -}; - export const onRenderBody = ({ setHeadComponents, setPostBodyComponents }) => { setHeadComponents([...headComponents]); setPostBodyComponents( diff --git a/client/src/components/Header/components/MenuButton.js b/client/src/components/Header/components/MenuButton.js new file mode 100644 index 0000000000..d9d1830b28 --- /dev/null +++ b/client/src/components/Header/components/MenuButton.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './menuButton.css'; + +const MenuButton = React.forwardRef((props, ref) => ( + +)); + +MenuButton.displayName = 'MenuButton'; +MenuButton.propTypes = { + className: PropTypes.string, + displayMenu: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired +}; + +export default MenuButton; diff --git a/client/src/components/Header/components/MenuLinks.js b/client/src/components/Header/components/MenuLinks.js new file mode 100644 index 0000000000..602cca4e46 --- /dev/null +++ b/client/src/components/Header/components/MenuLinks.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Link } from '../../helpers'; +import UserState from '../components/UserState'; + +import './menuLinks.css'; + +function MenuLinks(props) { + return ( + + ); +} + +MenuLinks.displayName = 'MenuLinks'; +MenuLinks.propTypes = { + className: PropTypes.string, + disableSettings: PropTypes.bool +}; + +export default MenuLinks; diff --git a/client/src/components/Header/components/NavMenu.js b/client/src/components/Header/components/NavMenu.js new file mode 100644 index 0000000000..9f0377fa23 --- /dev/null +++ b/client/src/components/Header/components/NavMenu.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import MenuButton from './MenuButton'; +import MenuLinks from './MenuLinks'; + +class NavigationMenu extends Component { + constructor(props) { + super(props); + this.state = { + displayMenu: false + }; + this.menuButtonRef = React.createRef(); + + this.handleClickOutside = this.handleClickOutside.bind(this); + this.toggleDisplayMenu = this.toggleDisplayMenu.bind(this); + } + + componentDidMount() { + document.addEventListener('click', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClickOutside); + } + + handleClickOutside(event) { + if ( + this.state.displayMenu && + this.menuButtonRef.current && + !this.menuButtonRef.current.contains(event.target) + ) { + this.toggleDisplayMenu(); + } + } + + toggleDisplayMenu() { + this.setState(({ displayMenu }) => ({ displayMenu: !displayMenu })); + } + + render() { + const { disableSettings } = this.props; + const { displayMenu } = this.state; + return ( + <> + + + + ); + } +} + +NavigationMenu.displayName = 'NavigationMenu'; +NavigationMenu.propTypes = { + disableSettings: PropTypes.bool +}; + +export default NavigationMenu; diff --git a/client/src/components/Header/components/menuButton.css b/client/src/components/Header/components/menuButton.css new file mode 100644 index 0000000000..7bfcf062f6 --- /dev/null +++ b/client/src/components/Header/components/menuButton.css @@ -0,0 +1,26 @@ + +#top-nav .menu-button { + color: white; + background-color: transparent; + margin: 0 20px 0 12px; + padding: 2px 14px; + border: 1px solid #fff; + border-radius: 3px; +} + +#top-nav .menu-button-open { + background: white; + color: #006400; +} + +@media (min-width: 735px) { + #top-nav .top-menu-button { + display: none; + } +} + +@media (max-width: 420px) { + #top-nav .menu-button { + margin: 0 10px 0 4px; + } +} diff --git a/client/src/components/Header/components/menuLinks.css b/client/src/components/Header/components/menuLinks.css new file mode 100644 index 0000000000..d2b9f5df14 --- /dev/null +++ b/client/src/components/Header/components/menuLinks.css @@ -0,0 +1,62 @@ +#top-right-nav { + display: flex; + margin: 0; + list-style: none; + justify-content: space-between; + align-items: center; + background-color: #006400; +} + +#top-right-nav li { + margin: 0; +} + +.top-right-nav-link { + max-height: var(--header-height); + color: #fff; + font-size: 18px; + padding: 8px 15px; +} + +.top-right-nav-link:hover, +.top-right-nav-link:focus, +.top-right-nav-link:active { + background-color: #fff; + color: #006400; + text-decoration: none; +} + +.user-state-spinner { + height: var(--header-height); + padding: 0 12px; +} + +.user-state-spinner > div { + animation-duration: 1.5s !important; +} + +#top-right-nav .signup-btn, +#top-right-nav .settings-link { + margin: 0 15px; +} + +#top-right-nav .user-avatar { + max-height: calc(var(--header-height) - 4px); +} + +@media (max-width: 734px) { + #top-right-nav { + position: absolute; + top: var(--header-height); + flex-direction: column; + width: 100vw; + height: min-content; + min-height: 160px; + padding: 10px 0; + display: none; + } + + #top-right-nav.top-nav-expanded { + display: flex; + } +} diff --git a/client/src/components/Header/header.css b/client/src/components/Header/header.css index b7e05691f5..1abf2f5cec 100644 --- a/client/src/components/Header/header.css +++ b/client/src/components/Header/header.css @@ -26,49 +26,6 @@ header { align-items: center; } -#top-nav .nav-logo { - max-height: 25px; - min-width: 35px; - margin: 0 5px; -} - -#top-right-nav { - display: flex; - margin: 0; - list-style: none; - justify-content: space-between; - align-items: center; - background-color: #006400; -} - -#top-right-nav li { - margin: 0; -} - -.top-right-nav-link { - max-height: var(--header-height); - color: #fff; - font-size: 18px; - padding: 8px 15px; -} - -.top-right-nav-link:hover, -.top-right-nav-link:focus, -.top-right-nav-link:active { - background-color: #fff; - color: #006400; - text-decoration: none; -} - -.user-state-spinner { - height: var(--header-height); - padding: 0 12px; -} - -.user-state-spinner > div { - animation-duration: 1.5s !important; -} - /* Search bar */ .fcc_searchBar { flex-grow: 1; @@ -127,6 +84,12 @@ header { /* Navbar logo */ +#top-nav .nav-logo { + max-height: 25px; + min-width: 35px; + margin: 0 5px; +} + .logoContainer { margin-right: 10px; } @@ -138,44 +101,11 @@ header { } } -#top-nav .menu-button { - color: white; - background-color: transparent; - margin: 0 20px 0 12px; - padding: 2px 14px; - border: 1px solid #fff; - border-radius: 3px; -} - -#top-nav .menu-button-open { - background: white; - color: #006400; -} - -#top-right-nav .signup-btn, -#top-right-nav .settings-link { - margin: 0 15px; -} - -#top-right-nav .user-avatar { - max-height: calc(var(--header-height) - 4px); -} - @media (max-width: 734px) { #top-nav { padding: 0; } - #top-right-nav { - position: absolute; - top: var(--header-height); - flex-direction: column; - width: 100vw; - height: min-content; - min-height: 160px; - padding: 10px 0; - } - #top-nav .nav-logo { margin: 0 0 0 10px; } @@ -185,14 +115,4 @@ header { #top-nav .nav-logo { margin: 0 0 0 5px; } - - #top-nav .menu-button { - margin: 0 10px 0 4px; - } - - .ais-Hits { - background-color: #fff; - left: 0.5em; - top: 2.4em; - } } diff --git a/client/src/components/Header/index.js b/client/src/components/Header/index.js index 1caf207ed1..44012ed169 100644 --- a/client/src/components/Header/index.js +++ b/client/src/components/Header/index.js @@ -1,145 +1,37 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { createSelector } from 'reselect'; -import Media from 'react-responsive'; import FCCSearch from 'react-freecodecamp-search'; +import NavigationMenu from './components/NavMenu'; import NavLogo from './components/NavLogo'; -import UserState from './components/UserState'; import { Link } from '../helpers'; import './header.css'; -import { - toggleDisplayMenu, - displayMenuSelector -} from '../layouts/components/guide/redux'; - -const mapStateToProps = createSelector( - displayMenuSelector, - displayMenu => ({ - displayMenu - }) -); - -const mapDispatchToProps = dispatch => - bindActionCreators({ toggleDisplayMenu }, dispatch); - const propTypes = { - disableMenuButtonBehavior: PropTypes.bool, disableSettings: PropTypes.bool, - displayMenu: PropTypes.bool, - mediaBreakpoint: PropTypes.string.isRequired, - toggleDisplayMenu: PropTypes.func.isRequired + navigationMenu: PropTypes.element }; -class Header extends Component { - constructor(props) { - super(props); - this.menuButtonRef = React.createRef(); - } - - componentDidMount() { - document.addEventListener('click', this.handleClickOutside); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside); - } - - handleClickOutside = event => { - if ( - !this.props.disableMenuButtonBehavior && - this.props.displayMenu && - this.menuButtonRef.current && - !this.menuButtonRef.current.contains(event.target) - ) { - this.props.toggleDisplayMenu(); - } - }; - - handleMediaChange = matches => { - if (!matches && this.props.displayMenu) { - this.props.toggleDisplayMenu(); - } - }; - - render() { - const { - disableMenuButtonBehavior, - disableSettings, - displayMenu, - mediaBreakpoint, - toggleDisplayMenu - } = this.props; - return ( -
- -
- ); - } +function Header(props) { + const { disableSettings, navigationMenu } = props; + return ( +
+ +
+ ); } Header.propTypes = propTypes; -Header.defaultProps = { - mediaBreakpoint: '734px' -}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(Header); +export default Header; diff --git a/client/src/components/layouts/Default.js b/client/src/components/layouts/Default.js index a1127b3506..86894edcce 100644 --- a/client/src/components/layouts/Default.js +++ b/client/src/components/layouts/Default.js @@ -56,8 +56,6 @@ const metaKeywords = [ const propTypes = { children: PropTypes.node.isRequired, - disableMenuButtonBehavior: PropTypes.bool, - disableSettings: PropTypes.bool, fetchUser: PropTypes.func.isRequired, flashMessages: PropTypes.arrayOf( PropTypes.shape({ @@ -70,7 +68,7 @@ const propTypes = { isOnline: PropTypes.bool.isRequired, isSignedIn: PropTypes.bool, landingPage: PropTypes.bool, - mediaBreakpoint: PropTypes.string, + navigationMenu: PropTypes.element.isRequired, onlineStatusChange: PropTypes.func.isRequired, removeFlashMessage: PropTypes.func.isRequired, showFooter: PropTypes.bool @@ -136,14 +134,12 @@ class DefaultLayout extends Component { render() { const { children, - disableSettings, hasMessages, flashMessages = [], removeFlashMessage, landingPage, showFooter = true, - mediaBreakpoint, - disableMenuButtonBehavior, + navigationMenu, isOnline, isSignedIn } = this.props; @@ -162,11 +158,7 @@ class DefaultLayout extends Component { > -
+
{hasMessages ? ( diff --git a/client/src/components/layouts/components/guide/NavMenu.js b/client/src/components/layouts/components/guide/NavMenu.js new file mode 100644 index 0000000000..b8d4c6a9c2 --- /dev/null +++ b/client/src/components/layouts/components/guide/NavMenu.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { createSelector } from 'reselect'; + +import MenuButton from '../../../../components/Header/components/MenuButton'; +import MenuLinks from '../../../../components/Header/components/MenuLinks'; + +import { toggleDisplayMenu, displayMenuSelector } from './redux'; + +const mapStateToProps = createSelector( + displayMenuSelector, + displayMenu => ({ + displayMenu + }) +); + +const mapDispatchToProps = dispatch => + bindActionCreators({ toggleDisplayMenu }, dispatch); + +function GuideNavigationMenu(props) { + const { displayMenu, toggleDisplayMenu } = props; + return ( + <> + + + + ); +} + +GuideNavigationMenu.displayName = 'GuideNavigationMenu'; +GuideNavigationMenu.propTypes = { + displayMenu: PropTypes.bool.isRequired, + toggleDisplayMenu: PropTypes.func.isRequired +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(GuideNavigationMenu); diff --git a/client/src/components/layouts/guide.css b/client/src/components/layouts/guide.css index e1df01d0fb..cbe292c5af 100644 --- a/client/src/components/layouts/guide.css +++ b/client/src/components/layouts/guide.css @@ -124,6 +124,15 @@ .content { height: auto; } + #top-right-nav.guide-top-nav { + display: none; + } +} + +@media (min-width: 993px) { + #top-nav .guide-menu-button { + display: none; + } } .content,