feat: universal navbar (#36744)
* feat: add universal nav * fix: add portfolio redirect
This commit is contained in:
committed by
mrugesh
parent
5d946f3d77
commit
e653235d94
41
client/src/components/Header/Header.test.js
Normal file
41
client/src/components/Header/Header.test.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* global expect */
|
||||
import React from 'react';
|
||||
import ShallowRenderer from 'react-test-renderer/shallow';
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
import Header from './';
|
||||
import NavLinks from './components/NavLinks';
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('renders to the DOM', () => {
|
||||
const shallow = new ShallowRenderer();
|
||||
shallow.render(<Header />);
|
||||
const result = shallow.getRenderOutput();
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<NavLinks />', () => {
|
||||
const root = TestRenderer.create(<NavLinks />).root;
|
||||
const aTags = root.findAllByType('a');
|
||||
|
||||
// reduces the aTags to href links
|
||||
const links = aTags.reduce((acc, item) => {
|
||||
acc.push(item._fiber.pendingProps.href);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const expectedLinks = ['/', '/portfolio'];
|
||||
|
||||
it('renders to the DOM', () => {
|
||||
expect(root).toBeTruthy();
|
||||
});
|
||||
it('has 2 a tags', () => {
|
||||
expect(aTags.length === 2).toBeTruthy();
|
||||
});
|
||||
|
||||
it('has link to portfolio', () => {
|
||||
// checks if all links in expected links exist in links
|
||||
expect(expectedLinks.every(elem => links.indexOf(elem) > -1)).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './menuButton.css';
|
||||
|
||||
const MenuButton = React.forwardRef((props, ref) => (
|
||||
<button
|
||||
aria-expanded={props.displayMenu}
|
||||
className={
|
||||
'menu-button' +
|
||||
(props.displayMenu ? ' menu-button-open' : '') +
|
||||
' ' +
|
||||
(props.className ? props.className : 'top-menu-button')
|
||||
}
|
||||
onClick={props.onClick}
|
||||
ref={ref}
|
||||
>
|
||||
Menu
|
||||
</button>
|
||||
));
|
||||
|
||||
MenuButton.displayName = 'MenuButton';
|
||||
MenuButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
displayMenu: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MenuButton;
|
@@ -1,40 +0,0 @@
|
||||
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 (
|
||||
<ul className={props.className} id='top-right-nav'>
|
||||
<li>
|
||||
<Link className='top-right-nav-link' to='/learn'>
|
||||
Learn
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='top-right-nav-link' external={true} to='/forum'>
|
||||
Forum
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='top-right-nav-link' external={true} to='/news'>
|
||||
News
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<UserState disableSettings={props.disableSettings} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
MenuLinks.displayName = 'MenuLinks';
|
||||
MenuLinks.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disableSettings: PropTypes.bool
|
||||
};
|
||||
|
||||
export default MenuLinks;
|
20
client/src/components/Header/components/NavLinks.js
Normal file
20
client/src/components/Header/components/NavLinks.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Link } from '../../helpers';
|
||||
|
||||
export function NavLinks() {
|
||||
return (
|
||||
<div className='main-nav-group'>
|
||||
<ul className={'nav-list display-flex'} role='menu'>
|
||||
<li className='nav-theme' role='menuitem'>
|
||||
<Link to='/'>Light</Link>
|
||||
</li>
|
||||
<li className='nav-portfolio' role='menuitem'>
|
||||
<Link to='/portfolio'>Portfolio</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NavLinks.displayName = 'NavLinks';
|
||||
export default NavLinks;
|
@@ -1,22 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||
const fCCglyph = 'https://s3.amazonaws.com/freecodecamp/FFCFire.png';
|
||||
|
||||
function NavLogo() {
|
||||
return (
|
||||
<picture>
|
||||
<source media='(max-width: 734px)' srcSet={fCCglyph} />
|
||||
<source media='(min-width: 735px)' srcSet={fCClogo} />
|
||||
<img
|
||||
alt='learn to code at freeCodeCamp logo'
|
||||
className='nav-logo'
|
||||
src={fCCglyph}
|
||||
src={fCClogo}
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
}
|
||||
|
||||
NavLogo.displayName = 'NavLogo';
|
||||
|
||||
export default NavLogo;
|
||||
|
@@ -1,65 +0,0 @@
|
||||
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 (
|
||||
<>
|
||||
<MenuButton
|
||||
displayMenu={displayMenu}
|
||||
onClick={this.toggleDisplayMenu}
|
||||
ref={this.menuButtonRef}
|
||||
/>
|
||||
<MenuLinks
|
||||
className={'top-nav' + (displayMenu ? ' top-nav-expanded' : '')}
|
||||
disableSettings={disableSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavigationMenu.displayName = 'NavigationMenu';
|
||||
NavigationMenu.propTypes = {
|
||||
disableSettings: PropTypes.bool
|
||||
};
|
||||
|
||||
export default NavigationMenu;
|
27
client/src/components/Header/components/UniversalNav.js
Normal file
27
client/src/components/Header/components/UniversalNav.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Link } from '../../helpers';
|
||||
import NavLogo from './NavLogo';
|
||||
import SearchBar from '../../search/searchBar/SearchBar';
|
||||
import NavLinks from './NavLinks';
|
||||
import './universalNav.css';
|
||||
|
||||
export function UniversalNav() {
|
||||
return (
|
||||
<nav className={'universal-nav nav-padding'} id='universal-nav'>
|
||||
<div className={'universal-nav-left'}>
|
||||
<SearchBar />
|
||||
</div>
|
||||
<div className='universal-nav-middle'>
|
||||
<Link className='universal-nav-logo' to='/'>
|
||||
<NavLogo />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='universal-nav-right main-nav'>
|
||||
<NavLinks />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
UniversalNav.displayName = 'UniversalNav';
|
||||
export default UniversalNav;
|
@@ -1,24 +0,0 @@
|
||||
#top-nav .menu-button {
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
margin: 0 20px 0 12px;
|
||||
padding: 2px 14px;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
#top-nav .menu-button-open {
|
||||
background: white;
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
@media (min-width: 735px) {
|
||||
#top-nav .top-menu-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
#top-nav .menu-button {
|
||||
margin: 0 10px 0 4px;
|
||||
}
|
||||
}
|
@@ -1,66 +0,0 @@
|
||||
#top-right-nav {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#top-right-nav li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.top-right-nav-link {
|
||||
max-height: var(--header-height);
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
padding: 8px 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-right-nav-link:hover,
|
||||
.top-right-nav-link:focus,
|
||||
.top-right-nav-link:active {
|
||||
background-color: #fff;
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-state-spinner {
|
||||
height: var(--header-height);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.user-state-spinner > div {
|
||||
animation-duration: 1.5s !important;
|
||||
}
|
||||
|
||||
.top-nav-expanded {
|
||||
background-color: var(--theme-color);
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
145
client/src/components/Header/components/universalNav.css
Normal file
145
client/src/components/Header/components/universalNav.css
Normal file
@@ -0,0 +1,145 @@
|
||||
.universal-nav {
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
/* overflow-y: hidden; */
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
height: calc(2 * var(--header-height));
|
||||
background: var(--theme-color);
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
color: var(--gray-00);
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.universal-nav a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.universal-nav-middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-right: 10px;
|
||||
letter-spacing: 0.4px;
|
||||
white-space: nowrap;
|
||||
-ms-overflow-scrolling: touch;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.universal-nav-logo {
|
||||
color: var(--gray-00);
|
||||
font-size: 1.7rem;
|
||||
line-height: 1em;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.5px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.universal-nav-logo:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-color);
|
||||
}
|
||||
|
||||
.universal-nav-logo img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 25px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
margin: 0 0 0 -12px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.nav-list li {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-list li a {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 8px 15px;
|
||||
color: var(--gray-00);
|
||||
opacity: 1;
|
||||
white-space: nowrap;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.nav-list li:hover {
|
||||
background: var(--gray-00);
|
||||
}
|
||||
|
||||
.nav-list li a:hover {
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
background: var(--gray-00);
|
||||
}
|
||||
|
||||
.universal-nav-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.universal-nav {
|
||||
padding: 0px 5px;
|
||||
}
|
||||
.universal-nav-logo {
|
||||
left: 5px;
|
||||
}
|
||||
.nav-list li a {
|
||||
padding: 8px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.universal-nav-logo {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: static;
|
||||
left: auto;
|
||||
top: auto;
|
||||
}
|
||||
.main-nav-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
.universal-nav-middle {
|
||||
flex: 1 0 33%;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
.universal-nav-left {
|
||||
flex: 1 0 33%;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.universal-nav-right {
|
||||
flex: 1 0 33%;
|
||||
margin-left: auto;
|
||||
}
|
||||
.universal-nav {
|
||||
height: var(--header-height);
|
||||
}
|
||||
}
|
@@ -4,59 +4,3 @@ header {
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
#top-nav {
|
||||
background: var(--theme-color);
|
||||
height: var(--header-height);
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
#top-nav .home-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#top-nav .home-link:hover {
|
||||
background-color: var(--theme-color);
|
||||
}
|
||||
|
||||
/* Navbar logo */
|
||||
|
||||
#top-nav .nav-logo {
|
||||
max-height: 25px;
|
||||
min-width: 35px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 735px) {
|
||||
.logoContainer {
|
||||
margin-right: 50px;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 734px) {
|
||||
#top-nav {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#top-nav .nav-logo {
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
#top-nav .nav-logo {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
}
|
||||
|
@@ -1,38 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Helmet from 'react-helmet';
|
||||
import SearchBar from '../search/searchBar/SearchBar';
|
||||
|
||||
import NavigationMenu from './components/NavMenu';
|
||||
import NavLogo from './components/NavLogo';
|
||||
import { Link } from '../helpers';
|
||||
import UniversalNav from './components/UniversalNav';
|
||||
|
||||
import './header.css';
|
||||
|
||||
const propTypes = {
|
||||
disableSettings: PropTypes.bool
|
||||
};
|
||||
|
||||
function Header(props) {
|
||||
const { disableSettings } = props;
|
||||
export function Header() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<style>{':root{--header-height: 38px}'}</style>
|
||||
</Helmet>
|
||||
<header>
|
||||
<nav id='top-nav'>
|
||||
<Link className='home-link' to='/'>
|
||||
<NavLogo />
|
||||
</Link>
|
||||
<SearchBar />
|
||||
<NavigationMenu disableSettings={disableSettings} />
|
||||
</nav>
|
||||
<UniversalNav />
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = propTypes;
|
||||
|
||||
Header.displayName = 'Header';
|
||||
export default Header;
|
||||
|
Reference in New Issue
Block a user