bufferapp/ui

View on GitHub
src/components/NavBar/NavBar.tsx

Summary

Maintainability
C
1 day
Test Coverage
C
72%
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import {
  Cross,
  Info as InfoIcon,
  ArrowLeft,
  Person as PersonIcon,
  Instagram as InstagramIcon,
  Twitter as TwitterIcon,
  Facebook as FacebookIcon,
  Pinterest as PinterestIcon,
  LinkedIn as LinkedInIcon,
} from '../Icon'

import {
  gray,
  blueDarker,
  grayLight,
  grayLighter,
  grayDark,
} from '../style/colors'

import { fontWeightMedium, fontFamily } from '../style/fonts'

import Link from '../Link'
import DropdownMenu from '../DropdownMenu'

import BufferLogo from './BufferLogo'
import NavBarMenu from './NavBarMenu/NavBarMenu'
import NavBarProducts from './NavBarProducts/NavBarProducts'

// @ts-expect-error TS(7006) FIXME: Parameter 'baseUrl' implicitly has an 'any' type.
export function getProductPath(baseUrl) {
  const result = baseUrl.match(/https*:\/\/(.+)\.buffer\.com/)
  let productPath = baseUrl
  if (result instanceof Array) {
    ;[, productPath] = result
  }
  return productPath
}

// @ts-expect-error TS(7006) FIXME: Parameter 'baseUrl' implicitly has an 'any' type.
function getRedirectUrl(baseUrl) {
  const productPath = getProductPath(baseUrl)
  return `https://${productPath}.buffer.com`
}

export function getLogoutUrl(baseUrl = '') {
  const productPath = getProductPath(baseUrl)
  return `https://login${
    productPath.includes('local') ? '.local' : ''
  }.buffer.com/logout?redirect=${getRedirectUrl(baseUrl)}`
}

// @ts-expect-error TS(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
export function getAccountUrl(baseUrl = '', user) {
  return `https://account.buffer.com?redirect=${getRedirectUrl(
    baseUrl,
  )}&username=${encodeURI(user.name)}`
}

export const ORG_SWITCHER = 'org_switcher'

export function getStopImpersonationUrl() {
  const { hostname } = window.location
  if (!hostname.endsWith('buffer.com')) {
    return null
  }

  return `https://admin${
    hostname.includes('local') ? '-next.local' : ''
  }.buffer.com/clearImpersonation`
}

const NavBarStyled = styled.nav`
  background: #fff;
  border-bottom: 1px solid ${gray};
  box-shadow: 0 1px 10px -5px rgba(0, 0, 0, 0.15);
  display: flex;
  height: 56px;
  justify-content: space-between;
  position: relative;
  width: 100vw;
`

const NavBarLeft = styled.div`
  display: flex;
`
const NavBarRight = styled.nav`
  display: flex;
`

const NavBarHelp = styled.a`
  align-items: center;
  color: #fff;
  color: ${(props): string => (props.  
// @ts-expect-error TS(2339) FIXME: Property 'active' does not exist on type 'ThemedSt... Remove this comment to see the full error message
active ? blueDarker : grayDark)};
  display: flex;
  font-size: 16px;
  font-family: ${fontFamily};
  font-weight: ${fontWeightMedium};
  height: 100%;
  padding: 0 24px;
  position: relative;
  text-decoration: none;
  z-index: 2;
  &:hover {
    color: ${(props): string => (props.    
// @ts-expect-error TS(2339) FIXME: Property 'active' does not exist on type 'ThemedSt... Remove this comment to see the full error message
active ? blueDarker : grayDark)};
    background-color: ${grayLighter};
  }
  cursor: pointer;
`

const NavBarHelpText = styled.span`
  margin-left: 8px;
`

const NavBarVerticalRule = styled.div`
  background-color: ${grayLight};
  height: 24px;
  margin-left: -1px;
  margin-right: -1px;
  position: relative;
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  z-index: 1;
`

/**
 * A11Y feature: A skip to main content link appears when a user is on a screen reader
 * and the link is in focus. To work properly, each page will need to have an element with the id main
 * example: <main id="main"></main> This feature is optional
 */
const SkipToMainLink = styled(Link)`
  position: absolute;
  top: -1000px;
  left: -1000px;
  height: 1px;
  width: 1px;
  overflow: hidden;

  :focus {
    left: auto;
    top: auto;
    position: relative;
    height: auto;
    width: auto;
    overflow: visible;
    margin: auto;
    margin-left: 10px;
  }
`

// @ts-expect-error TS(7006) FIXME: Parameter 'ignoreMenuItems' implicitly has an 'any... Remove this comment to see the full error message
export function appendMenuItem(ignoreMenuItems, menuItem) {
  if (!ignoreMenuItems) {
    return menuItem
  }

  return ignoreMenuItems.includes(menuItem.id) ? null : menuItem
}

// @ts-expect-error TS(7006) FIXME: Parameter 'item' implicitly has an 'any' type.
function getNetworkIcon(item) {
  if (!item.network) return null

  switch (item.network) {
    case 'instagram':
      return <InstagramIcon size="medium" />
    case 'twitter':
      return <TwitterIcon size="medium" />
    case 'facebook':
      return <FacebookIcon size="medium" />
    case 'pinterest':
      return <PinterestIcon size="medium" />
    case 'linkedin':
      return <LinkedInIcon size="medium" />
    default:
      break
  }
}

// @ts-expect-error TS(7006) FIXME: Parameter 'orgSwitcher' implicitly has an 'any' ty... Remove this comment to see the full error message
export function appendOrgSwitcher(orgSwitcher) {
  if (!orgSwitcher || !orgSwitcher.menuItems) {
    return []
  }

  // @ts-expect-error TS(7006) FIXME: Parameter 'item' implicitly has an 'any' type.
  return orgSwitcher.menuItems.map((item, index) => {
    item.type = ORG_SWITCHER
    if (orgSwitcher.title && index === 0) {
      item.hasDivider = true
      item.dividerTitle = orgSwitcher.title
    }
    if (item.subItems) {
      // @ts-expect-error TS(7006) FIXME: Parameter 'subItem' implicitly has an 'any' type.
      item.subItems.forEach((subItem) => {
        subItem.icon = getNetworkIcon(subItem)
      })
    }
    if (!item.subItems || item.subItems.length === 0) {
      item.defaultTooltipMessage = 'No channels connected yet.'
    }

    return item
  })
}

/**
 * The NavBar is not consumed alone, but instead is used by the AppShell component. Go check out the AppShell component to learn more.
 */
class NavBar extends React.Component {
  // @ts-expect-error TS(7006) FIXME: Parameter 'nextProps' implicitly has an 'any' type... Remove this comment to see the full error message
  shouldComponentUpdate(nextProps) {
    return (
      // @ts-expect-error TS(2339) FIXME: Property 'user' does not exist on type 'Readonly<{... Remove this comment to see the full error message
      nextProps.user.name !== this.props.user.name ||
      // @ts-expect-error TS(2339) FIXME: Property 'user' does not exist on type 'Readonly<{... Remove this comment to see the full error message
      nextProps.user.email !== this.props.user.email ||
      // @ts-expect-error TS(2339) FIXME: Property 'isImpersonation' does not exist on type ... Remove this comment to see the full error message
      nextProps.isImpersonation !== this.props.isImpersonation ||
      // @ts-expect-error TS(2339) FIXME: Property 'products' does not exist on type 'Readon... Remove this comment to see the full error message
      nextProps.products !== this.props.products ||
      // @ts-expect-error TS(2339) FIXME: Property 'orgSwitcher' does not exist on type 'Rea... Remove this comment to see the full error message
      nextProps.orgSwitcher !== this.props.orgSwitcher
    )
  }

  render() {
    const {
      // @ts-expect-error TS(2339) FIXME: Property 'products' does not exist on type 'Readon... Remove this comment to see the full error message
      products,
      // @ts-expect-error TS(2339) FIXME: Property 'activeProduct' does not exist on type 'R... Remove this comment to see the full error message
      activeProduct,
      // @ts-expect-error TS(2339) FIXME: Property 'user' does not exist on type 'Readonly<{... Remove this comment to see the full error message
      user,
      // @ts-expect-error TS(2339) FIXME: Property 'helpMenuItems' does not exist on type 'R... Remove this comment to see the full error message
      helpMenuItems,
      // @ts-expect-error TS(2339) FIXME: Property 'onLogout' does not exist on type 'Readon... Remove this comment to see the full error message
      onLogout,
      // @ts-expect-error TS(2339) FIXME: Property 'displaySkipLink' does not exist on type ... Remove this comment to see the full error message
      displaySkipLink,
      // @ts-expect-error TS(2339) FIXME: Property 'isImpersonation' does not exist on type ... Remove this comment to see the full error message
      isImpersonation,
      // @ts-expect-error TS(2339) FIXME: Property 'orgSwitcher' does not exist on type 'Rea... Remove this comment to see the full error message
      orgSwitcher,
    } = this.props

    const orgSwitcherHasItems =
      orgSwitcher && orgSwitcher.menuItems && orgSwitcher.menuItems.length > 0

    return (
      <NavBarStyled aria-label="Main menu">
        <NavBarLeft>
          {displaySkipLink && (
            <SkipToMainLink href="#main">Skip to main content</SkipToMainLink>
          )}
          <BufferLogo />
          <NavBarVerticalRule />
          <NavBarProducts products={products} activeProduct={activeProduct} />
        </NavBarLeft>
        <NavBarRight>
          {helpMenuItems && (
            <DropdownMenu
              // @ts-expect-error TS(2322) FIXME: Type '{ xPosition: string; ariaLabel: string; aria... Remove this comment to see the full error message
              xPosition="right"
              ariaLabel="Help Menu"
              ariaLabelPopup="Help"
              menubarItem={
                <NavBarHelp>
                  <InfoIcon />
                  <NavBarHelpText>Help</NavBarHelpText>
                </NavBarHelp>
              }
              items={helpMenuItems}
            />
          )}
          <NavBarVerticalRule />
          <DropdownMenu
            // @ts-expect-error TS(2322) FIXME: Type '{ xPosition: string; ariaLabel: string; aria... Remove this comment to see the full error message
            xPosition="right"
            ariaLabel="Account Menu"
            ariaLabelPopup="Account"
            horizontalOffset="-16px"
            isImpersonation={isImpersonation}
            menubarItem={
              <NavBarMenu user={user} isImpersonation={isImpersonation} />
            }
            items={[
              ...appendOrgSwitcher(orgSwitcher),
              appendMenuItem(user.ignoreMenuItems, {
                id: 'account',
                title: 'Account',
                icon: <PersonIcon color={gray} />,
                hasDivider: orgSwitcherHasItems,
                onItemClick: () => {
                  window.location.assign(
                    // @ts-expect-error TS(2339) FIXME: Property 'user' does not exist on type 'Readonly<{... Remove this comment to see the full error message
                    getAccountUrl(window.location.href, this.props.user),
                  )
                },
              }),
              ...user.menuItems,
              appendMenuItem(
                user.ignoreMenuItems,
                isImpersonation
                  ? {
                      id: 'Stop Impersonation',
                      title: 'Stop Impersonation',
                      icon: <Cross color={gray} />,
                      hasDivider: user.menuItems && user.menuItems.length > 0,
                      onItemClick: () => {
                        // @ts-expect-error TS(2345) FIXME: Argument of type 'string | null' is not assignable... Remove this comment to see the full error message
                        window.location.assign(getStopImpersonationUrl())
                      },
                    }
                  : {
                      id: 'logout',
                      title: 'Logout',
                      icon: <ArrowLeft color={gray} />,
                      hasDivider: user.menuItems && user.menuItems.length > 0,
                      onItemClick: () => {
                        if (typeof onLogout === 'function') onLogout()
                        window.location.assign(
                          getLogoutUrl(window.location.href),
                        )
                      },
                    },
              ),
            ].filter((e) => e)}
          />
        </NavBarRight>
      </NavBarStyled>
    )
  }
}

// @ts-expect-error TS(2339) FIXME: Property 'propTypes' does not exist on type 'typeo... Remove this comment to see the full error message
NavBar.propTypes = {
  /** The list of available products */
  products: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.string,
      isNew: PropTypes.bool,
      href: PropTypes.string,
    }),
  ),

  /** The currently active (highlighted) product in the `NavBar`. */
  activeProduct: PropTypes.oneOf(['publish', 'analyze', 'engage']),

  user: PropTypes.shape({
    name: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
    /** If missing we will use Gravatar to get the user avatar by email */
    avatar: PropTypes.string,
    /** If missing we will use Gravatar to get the user avatar by email */
    ignoreMenuItems: PropTypes.arrayOf(PropTypes.string),
    menuItems: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.string.isRequired,
        title: PropTypes.string.isRequired,
        component: PropTypes.func,
        hasDivider: PropTypes.bool,
        onItemClick: PropTypes.func,
      }),
    ).isRequired,
  }).isRequired,
  helpMenuItems: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
      component: PropTypes.node,
      hasDivider: PropTypes.bool,
      onItemClick: PropTypes.func,
    }),
  ),
  isImpersonation: PropTypes.bool,

  onLogout: PropTypes.func,
  displaySkipLink: PropTypes.bool,

  /** Optional menu for selecting the user's organization */
  orgSwitcher: PropTypes.shape({
    title: PropTypes.string.isRequired,
    menuItems: PropTypes.arrayOf(
      PropTypes.shape({
        id: PropTypes.string.isRequired,
        title: PropTypes.string.isRequired,
        selected: PropTypes.bool.isRequired,
        onItemClick: PropTypes.func,
      }),
    ).isRequired,
  }),
}

// @ts-expect-error TS(2339) FIXME: Property 'defaultProps' does not exist on type 'ty... Remove this comment to see the full error message
NavBar.defaultProps = {
  products: [],
  activeProduct: undefined,
  helpMenuItems: null,
  isImpersonation: false,
  onLogout: undefined,
  displaySkipLink: false,
  orgSwitcher: undefined,
}

export default NavBar