Source

src/components/Layout/Navbar.jsx

import React, { useState, useEffect } from 'react'
import styled, { keyframes } from 'styled-components'
import colors from '../../config/colors'
import { hamburger, close } from '../../config/svg'

const NavBarOuterContainer = styled.div`
  width: 100%;
  background: ${(props) => (props.light ? colors.skyblue : colors.darkgray)};
  border-bottom: ${(props) => (props.light ? 'none' : '1px solid black')};
  z-index: 1;
  position: relative;
  pointer-events: none;
  @media print {
    display: none;
  }
`

const NavbarContainer = styled.nav`
  width: 100%;
  max-width: ${(props) => (props.light ? '1140px' : 'initial')};
  display: flex;
  margin: 0 auto;
  color: #0d0d0d;
  z-index: 249;
  pointer-events: none;
  div,
  ul {
    pointer-events: all;
  }
  ul {
    list-style: none;
    margin: 0;
    order: ${(props) => (props.light ? '1' : 'initial')};
    @media (min-width: 1025px) {
      display: flex;
      margin: ${(props) => (props.light ? '0 0 0 auto' : '0px')};
    }
    li {
      @media (min-width: 1024px) {
        height: 50px;
        display: flex;
      }
      align-items: center;
      box-sizing: border-box;
    }
  }
  a,
  button {
    margin: auto;
    height: 100%;
    display: flex;
    align-items: center;
    flex: 1;
    color: ${(props) => (props.light ? colors.darkgray : colors.lightgray)};
    text-align: center;
    padding: 10px;
    text-decoration: none;
    transition: 250ms all;
    background: none;
    border: none;
    font-family: Lato, sans-serif;
    font-size: 0.9rem;
    line-height: 1.5;
    letter-spacing: 1.75px;
    font-weight: 400;
    font-stretch: normal;
    font-opacity: 30%;
    cursor: pointer;
    &:hover {
      background: ${(props) =>
        props.light ? colors.teal : colors.lightgray}55;
      color: ${(props) => (props.light ? colors.teal : colors.yellow)};
    }
    &.active {
      background: ${(props) => (props.light ? colors.teal : colors.darkgray)};
      color: #eee;
    }
  }
  button {
    padding-right: 20px;
  }
  button::after {
    content: '❱';
    transform: rotate(90deg) scaleY(0.75) translateX(2px);
    padding-bottom: 15px;
    color: ${(props) => (props.light ? colors.teal : colors.red)};
  }
  button.active::after {
    color: #eee;
  }
  @media (max-width: 1025px) {
    button {
      color: ${(props) => (props.light ? colors.darkgray : colors.lightgray)};
      font-size: 0.9rem;
      color: white;
      font-weight: bold;
      font-size: 2rem;
      text-align: left;
      margin: 0;
    }
    button::after {
      content: '❱';
      transform: scaleY(0.75) translate(5px, 5px);
      padding-bottom: 15px;
      color: ${colors.white};
    }
  }
`

const NavLogo = styled.div`
  color: white;
  display: flex;
  align-items: center;
  padding: 0 0 0 2px;
  font-size: 20px;
  span {
    padding-left: 10px;
  }
`

const NavItems = styled.div`
  @media (max-width: 1025px) {
    position: absolute;
    top: 50px;
    padding-left: ${({ navOpen }) => (navOpen ? '-5vw' : '5vw')};
    transition: 250ms all;
    width: 100%;
    height: calc(100vh - 50px);
    background: ${(props) => (props.light ? colors.teal : colors.black)}ee;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    ul {
      flex-direction: column;
      z-index: 500;
      li {
        display: block;
        a {
          color: white;
          font-weight: bold;
          font-size: 2rem;
          &:hover {
            background: ${(props) =>
              props.light ? colors.lightblue : colors.black}ee;
          }
        }
      }
    }
  }
`
const fadeIn = keyframes`
  from {
    opacity:0;
  }

  to {
    opacity:1;
  }
`

const Shade = styled.button`
  background: rgb(0, 0, 0);
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.5) 0%,
    rgba(0, 0, 0, 0.25) 100%
  );
  width: 100%;
  height: 100vh;
  position: fixed;
  left: 0;
  top: 50px;
  padding: 0;
  margin: 0;
  z-index: 100;
  border: none;
  animation: ${fadeIn} 250ms linear 1;
  cursor: pointer;
  @media (max-width: 1025px) {
    display: none;
  }
`

const SuperDropdown = styled.div`
  width: 100%;
  max-width: ${(props) => (props.light ? '1140px' : 'none')};
  display: flex;
  margin: 0 auto;
  background: ${(props) => (props.light ? colors.teal : colors.darkgray)};
  position: fixed;
  left: ${(props) => (props.light ? '50%' : '0')};
  top: 50px;
  transform: ${(props) => (props.light ? 'translateX(-50%)' : 'none')};
  z-index: 110;
  border: 1px solid black;
  animation: ${fadeIn} 250ms linear 1;
  user-select: none;
  a {
    color: ${colors.white};
  }
  @media (max-width: 1025px) {
    display: none;
  }
`

const MobileNestedNav = styled.div`
  position: fixed;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  span {
    height: fit-content;
  }
  a {
    letter-spacing: initial;
    color: white;
    line-height: 1;
    padding: 0 2em 0 0;
    max-width: 12ch;
    text-align: left;
    span {
      display: none;
    }
  }
  p {
    color: white;
    font-size: 1.2rem;
  }
  @media (min-width: 1025px) {
    display: none;
  }
`

const PageSection = styled.span`
  padding: 0.5em 2em;
  a {
    font-size: 1.35rem;
    font-weight: bold;
    text-decoration: none;
    line-height: 2;
    border-bottom: 2px solid rgba(0, 0, 0, 0);
    transition: 250ms all;
    span {
      color: ${colors.red};
      padding-left: 0.25em;
      transition: 250ms all;
    }
    &:hover {
      border-bottom: 2px solid ${colors.yellow};
      span {
        padding-left: 0.5em;
      }
    }
  }
  p {
    max-width: 35ch;
    padding-bottom: 1.5em;
    color: ${colors.white};
  }
`

const NavHamburger = styled.button`
  margin: 0 0 0 auto !important;
  max-height: 50px;
  max-width: 50px;
  padding: 0.125em !important;
  pointer-events: all;
  overflow: hidden;
  &::after {
    display: none;
  }
  svg g {
    fill: ${(props) => (props.light ? colors.black : colors.white)};
  }
`
/**
 * @typedef {Object} DropdownEntry
 * 
 * An entry in for dropdown sub-links
 * 
 * @property {string} header - Title for dropdown 
 * @property {string} desc - Dropdown description
 * @property {string} link - Link for dropdown
 */

/**
 * An object to be rendered as titles for dropdowns
 * with DropdownEntry(ies) as content in sub-dropdowns
 * @typedef {Object.<string, DropdownEntry>} DropdownContent
 */
const defaultDropDowns = {
  LEARN: [
    {
      header: 'Tutorials & Toolkit',
      desc: 'Tutorials for using the Atlas, for all levels.',
      link: '/learn',
    },
    {
      header: 'Methods',
      desc: 'Overview of the methods behind our analytics.',
      link: '/methods',
    },
    {
      header: 'FAQ',
      desc: 'Frequently asked questions and answers.',
      link: '/faq',
    },
  ],
  ABOUT: [
    {
      header: 'Overview',
      desc: 'What the Atlas is and what can it help you learn.',
      link: '/about',
    },
    {
      header: 'Team',
      desc: 'The core team and contributors behind the Atlas project.',
      link: '/about#team',
    },
    {
      header: 'Advisory Board',
      desc: 'The community advisory board driving engagement from 2021 onward.',
      link: '/cab',
    },
    {
      header: 'Tech',
      desc: 'About the US Covid Atlas stack.',
      link: '/tech'
    },
  ],
  DATA: [
    {
      header: 'Docs',
      desc: 'Detailed documentation on the data sources used in the Atlas including sources, limitations, and how to access to raw data.',
      link: '/docs',
    },
    {
      header: 'Download',
      desc: 'Interactive data downloader to access cleaned CSV data on the Atlas.',
      link: '/download',
    },
  ],
  INSIGHTS: [
    {
      header: 'Research',
      desc: 'Peer-reviewed academic research related to efforts to better understand COVID-19 and the tools we use.',
      link: '/insights#research',
    },
    {
      header: 'Viz',
      desc: 'Data visualizations that highlight the challenges, reality, and complexity of COVID-19.',
      link: '/insights#viz',
    },
    {
      header: 'Media',
      desc: 'Media and press coverage of our research, insights, and data.',
      link: '/insights#media',
    },
    {
      header: 'Blog',
      desc: "Snapshots and short-form articles of what we're seeing and how we're working.",
      link: 'https://medium.com/covidatlas',
      external: true,
    },
  ],
  STORIES: [
    {
      header: 'Explore Map',
      desc: 'View stories from across the country on the Atlas map.',
      link: '/map',
    },
    {
      header: 'Archive',
      desc: 'Explore stories by tag, topic, theme, and area in the complete archive.',
      link: '/archive',
    },
  ],
  // 'METHODS':[
  //   {
  //     header: 'Mapping',
  //     desc: 'How we use spatial statistics, cartography, and geographic information science to produce the Atlas.',
  //     link: '/methods#mapping'
  //   },
  //   {
  //     header: 'Infrastructure',
  //     desc: 'The data and statistical backbone of the Atlas.',
  //     link: '/methods#infrastructure'
  //   },
  //   {
  //     header: 'Data Collection',
  //     desc: 'Our structure for daily data updates, scraping, collection, and aggregation.',
  //     link: '/methods#data'
  //   },
  // ],
}

/**
 * 
 * @component
 * @category Components/Layout
 * 
 * @param {Object} props 
 * @param {string} props.light - Page theme light or dark
 * @param {DropdownContent} pageDropDowns - Object of dropdowns to render, defaults to defaultDropDowns in src/Components/Layout/Nav.jsx
 * @example 
 * function MyComponent(){
 *  return(
 *   <Nav light />
 *  )
 * }
 */
function NavBar({light, pageDropDowns=defaultDropDowns}) {
  const [currentDropdown, setCurrentDropdown] = useState(false)
  const [dims, setDims] = useState({
    height: window.innerHeight,
    width: window.innerWidth,
  })
  const [navOpen, setNavOpen] = useState(false)

  const listener = (e) => {
    setCurrentDropdown(false)
    setNavOpen(false)
    document.removeEventListener('scroll', listener)
  }
  const handleOpenDropdown = (page) => {
    setCurrentDropdown(page)
    document.addEventListener('scroll', listener)
  }

  const toggleNavOpen = () => {
    setNavOpen((prev) => {
      if (prev) {
        setCurrentDropdown(false)
        return !prev
      } else {
        return !prev
      }
    })
  }

  const handleResize = () => {
    setCurrentDropdown(false)
    setNavOpen(false)
    setDims({
      height: window.innerHeight,
      width: window.innerWidth,
    })
  }

  useEffect(() => {
    window.addEventListener('resize', handleResize)
  }, [])

  const NavButton = ({ page = '' }) => (
    <button
      className={currentDropdown === page ? 'active' : ''}
      onClick={() => handleOpenDropdown(page)}
      onMouseOver={() => handleOpenDropdown(page)}
    >
      {page}
    </button>
  )

  return (
    <>
      <NavBarOuterContainer light={light}>
        <NavbarContainer light={light}>
          <NavLogo>
            <a href="/">
              <img
                src={`${process.env.PUBLIC_URL}/favicon/android-icon-192x192.png`}
                style={{ height: '30px', paddingRight: '5px' }}
                alt="US Covid Atlas Logo"
              />
              <span>US COVID ATLAS</span>
            </a>
          </NavLogo>
          {(navOpen || dims.width > 1024) && (
            <NavItems light={light} navOpen={currentDropdown}>
              <ul>
                <li>
                  <a href="/map">MAP</a>
                </li>
                <li>
                  <NavButton page="LEARN" />
                </li>
                <li>
                  <NavButton page="ABOUT" />
                </li>
                <li>
                  <NavButton page="DATA" />
                </li>
                <li>
                  <NavButton page="INSIGHTS" />
                </li>
                <li>
                  <NavButton page="STORIES" />
                </li>
                <li>
                  <a href="/contact">CONTACT</a>
                </li>
              </ul>
            </NavItems>
          )}
          {currentDropdown && (
            <MobileNestedNav light={light}>
              <p>Pages</p>
              {pageDropDowns[currentDropdown].map((entry) => (
                <PageSection key={entry.header}>
                  <a href={entry.link} target={entry.external ? '_blank' : ""}>
                    {entry.header}{entry.external && ' ↗'}
                    <span>❱</span>
                  </a>
                </PageSection>
              ))}
            </MobileNestedNav>
          )}
          {dims.width <= 1024 && (
            <NavHamburger onClick={toggleNavOpen} light={light}>
              {navOpen ? close : hamburger}
            </NavHamburger>
          )}
        </NavbarContainer>
      </NavBarOuterContainer>
      {currentDropdown && (
        <SuperDropdown light={light}>
          {pageDropDowns[currentDropdown].map((entry) => (
            <PageSection key={entry.header}>
              <a href={entry.link} target={entry.external ? '_blank' : ""}>
                {entry.header}{entry.external && ' ↗'}
                <span>❱</span>
              </a>
              <p>{entry.desc}</p>
            </PageSection>
          ))}
        </SuperDropdown>
      )}
      {currentDropdown && (
        <Shade
          onClick={() => setCurrentDropdown(false)}
          onMouseOver={() => setCurrentDropdown(false)}
        />
      )}
    </>
  )
}
export default NavBar