Skip to content
Snippets Groups Projects
TopBar.js 9.92 KiB
Newer Older
import React from 'react'
import PropTypes from 'prop-types'
import intl from 'react-intl-universal'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import IconButton from '@material-ui/core/IconButton'
esikkala's avatar
esikkala committed
import Typography from '@material-ui/core/Typography'
import MenuItem from '@material-ui/core/MenuItem'
import Menu from '@material-ui/core/Menu'
esikkala's avatar
esikkala committed
import { makeStyles } from '@material-ui/core/styles'
import MoreIcon from '@material-ui/icons/MoreVert'
import Button from '@material-ui/core/Button'
import { Link, NavLink } from 'react-router-dom'
esikkala's avatar
esikkala committed
import TopBarSearchField from '../../main_layout/TopBarSearchField'
import TopBarInfoButton from '../../main_layout/TopBarInfoButton'
import TopBarLanguageButton from '../../main_layout/TopBarLanguageButton'
import Divider from '@material-ui/core/Divider'
import { has } from 'lodash'
esikkala's avatar
esikkala committed
import secoLogo from '../../../img/logos/seco-logo-48x50.png'
import { showLanguageButton } from '../../../configs/sampo/GeneralConfig'
esikkala's avatar
esikkala committed
const useStyles = makeStyles((theme) => ({
    flexGrow: 1
  toolbar: {
    paddingLeft: theme.spacing(1.5),
    paddingRight: theme.spacing(1.5)
  sectionDesktop: {
    display: 'none',
esikkala's avatar
esikkala committed
    [theme.breakpoints.up('lg')]: {
      display: 'flex'
    }
  link: {
    textDecoration: 'none'
  },
  sectionMobile: {
    display: 'flex',
esikkala's avatar
esikkala committed
    [theme.breakpoints.up('lg')]: {
      display: 'none'
    }
  homeButtonText: {
    whiteSpace: 'nowrap',
    [theme.breakpoints.down('sm')]: {
      fontSize: '1rem'
    }
  },
    whiteSpace: 'nowrap',
esikkala's avatar
esikkala committed
    color: 'white !important',
    border: `1px solid ${theme.palette.primary.main}`
  appBarButtonActive: {
    border: '1px solid white'
esikkala's avatar
esikkala committed
  },
  appBarDivider: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    borderLeft: '2px solid white'
  },
  secoLogo: {
    marginLeft: theme.spacing(1),
    [theme.breakpoints.down('md')]: {
      display: 'none'
    }
esikkala's avatar
esikkala committed
}))
esikkala's avatar
esikkala committed
/**
 * Responsive app bar with a search field, perspective links, info links and a language
 * selector. Based on Material-UI's App Bar component.
 */
esikkala's avatar
esikkala committed
const TopBar = props => {
  const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null)
  const isMobileMenuOpen = Boolean(mobileMoreAnchorEl)
  const { perspectives, currentLocale, availableLocales, rootUrl } = props
  const classes = useStyles()
  const handleMobileMenuOpen = event => setMobileMoreAnchorEl(event.currentTarget)
  const handleMobileMenuClose = () => setMobileMoreAnchorEl(null)
esikkala's avatar
esikkala committed
  const clientFSMode = props.location.pathname.indexOf('clientFS') !== -1
  // https://material-ui.com/components/buttons/#third-party-routing-library
esikkala's avatar
esikkala committed
  const AdapterLink = React.forwardRef((props, ref) => <Link innerRef={ref} {...props} />)
  const AdapterNavLink = React.forwardRef((props, ref) => <NavLink innerRef={ref} {...props} />)
esikkala's avatar
esikkala committed
  const renderMobileMenuItem = perspective => {
    const searchMode = perspective.id.startsWith('clientFS') ? 'federated-search' : 'faceted-search'
    if (has(perspective, 'externalUrl')) {
esikkala's avatar
esikkala committed
          className={classes.link}
          key={perspective.id}
          href={perspective.externalUrl}
          target='_blank'
          rel='noopener noreferrer'
        >
          <MenuItem>
            {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
          </MenuItem>
        </a>
    } else {
        <MenuItem
          key={perspective.id}
esikkala's avatar
esikkala committed
          component={AdapterLink}
          to={`${props.rootUrl}/${perspective.id}/${searchMode}`}
          onClick={handleMobileMenuClose}
          {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
        </MenuItem>
esikkala's avatar
esikkala committed
  const renderDesktopTopMenuItem = perspective => {
    const searchMode = perspective.id.startsWith('clientFS') ? 'federated-search' : 'faceted-search'
    if (has(perspective, 'externalUrl')) {
esikkala's avatar
esikkala committed
          className={classes.link}
          key={perspective.id}
          href={perspective.externalUrl}
          target='_blank'
          rel='noopener noreferrer'
        >
          <Button
esikkala's avatar
esikkala committed
            className={classes.appBarButton}
esikkala's avatar
esikkala committed
          >
            {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
          </Button>
        </a>
    } else {
        <Button
          key={perspective.id}
esikkala's avatar
esikkala committed
          className={classes.appBarButton}
          component={AdapterNavLink}
          to={`${props.rootUrl}/${perspective.id}/${searchMode}`}
          isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/${perspective.id}`)}
          activeClassName={classes.appBarButtonActive}
          {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()}
        </Button>
esikkala's avatar
esikkala committed
  const renderMobileMenu = perspectives =>
    <Menu
esikkala's avatar
esikkala committed
      anchorEl={mobileMoreAnchorEl}
      anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
      transformOrigin={{ vertical: 'top', horizontal: 'right' }}
esikkala's avatar
esikkala committed
      open={isMobileMenuOpen}
      onClose={handleMobileMenuClose}
      {perspectives.map(perspective => perspective.isHidden ? null : renderMobileMenuItem(perspective))}
esikkala's avatar
esikkala committed
      <Divider />
      <MenuItem
        key='feedback'
esikkala's avatar
esikkala committed
        component={AdapterLink}
        to={`${props.rootUrl}/feedback`}
        onClick={handleMobileMenuClose}
esikkala's avatar
esikkala committed
        {intl.get('topBar.feedback').toUpperCase()}
esikkala's avatar
esikkala committed
      </MenuItem>
esikkala's avatar
esikkala committed
      <MenuItem
        key={0}
esikkala's avatar
esikkala committed
        component={AdapterLink}
        to={`${props.rootUrl}/about`}
        onClick={handleMobileMenuClose}
esikkala's avatar
esikkala committed
      >
        {intl.get('topBar.info.aboutThePortal').toUpperCase()}
esikkala's avatar
esikkala committed
      </MenuItem>
esikkala's avatar
esikkala committed
        className={classes.link}
esikkala's avatar
esikkala committed
        key={1}
esikkala's avatar
esikkala committed
        href={intl.get('topBar.info.blogUrl')}
esikkala's avatar
esikkala committed
        target='_blank'
        rel='noopener noreferrer'
        onClick={handleMobileMenuClose}
esikkala's avatar
esikkala committed
      >
        <MenuItem>
esikkala's avatar
esikkala committed
          {intl.get('topBar.info.blog').toUpperCase()}
esikkala's avatar
esikkala committed
        </MenuItem>
      </a>
esikkala's avatar
esikkala committed
      <MenuItem
        key='info'
esikkala's avatar
esikkala committed
        component={AdapterLink}
        to={`${props.rootUrl}/instructions`}
        onClick={handleMobileMenuClose}
esikkala's avatar
esikkala committed
        {intl.get('topBar.instructions').toUpperCase()}
esikkala's avatar
esikkala committed
      </MenuItem>
    </Menu>

esikkala's avatar
esikkala committed
  return (
    <div className={classes.root}>
      {/* Add an empty Typography element to ensure that that the MuiTypography class is loaded for
esikkala's avatar
esikkala committed
         any lower level components that use MuiTypography class only in translation files */}
esikkala's avatar
esikkala committed
      <Typography />
      <AppBar position='absolute'>
        <Toolbar className={classes.toolbar}>
          <Button component={AdapterLink} to='/'>
            <Typography className={classes.homeButtonText} variant='h6'>{intl.get('appTitle.short')}</Typography>
          </Button>
esikkala's avatar
esikkala committed
          {!clientFSMode &&
            <TopBarSearchField
              fetchFullTextResults={props.fetchFullTextResults}
              clearResults={props.clearResults}
              xsScreen={props.xsScreen}
              rootUrl={rootUrl}
            />}
esikkala's avatar
esikkala committed
          <div className={classes.grow} />
          <div className={classes.sectionDesktop}>
            {perspectives.map((perspective, index) => perspective.isHidden ? null : renderDesktopTopMenuItem(perspective, index))}
esikkala's avatar
esikkala committed
            <div className={classes.appBarDivider} />
            <Button
              className={classes.appBarButton}
              component={AdapterNavLink}
              to={`${props.rootUrl}/feedback`}
              isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/feedback`)}
              activeClassName={classes.appBarButtonActive}
            >
              {intl.get('topBar.feedback')}
esikkala's avatar
esikkala committed
            <TopBarInfoButton rootUrl={props.rootUrl} />
            <Button
              className={classes.appBarButton}
              component={AdapterNavLink}
              to={`${props.rootUrl}/instructions`}
              isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/instructions`)}
              activeClassName={classes.appBarButtonActive}
esikkala's avatar
esikkala committed
              {intl.get('topBar.instructions')}
            </Button>
            {showLanguageButton &&
              <TopBarLanguageButton
                currentLocale={currentLocale}
                availableLocales={availableLocales}
                loadLocales={props.loadLocales}
                location={props.location}
              />}
          </div>
          <a
            className={classes.secoLogo}
            href='https://seco.cs.aalto.fi'
            target='_blank'
            rel='noopener noreferrer'
          >
            <Button><img src={secoLogo} /></Button>
          </a>
          <div className={classes.sectionMobile}>
            <IconButton aria-haspopup='true' onClick={handleMobileMenuOpen} color='inherit'>
              <MoreIcon />
            </IconButton>
          </div>
        </Toolbar>
      </AppBar>
      {renderMobileMenu(perspectives)}
    </div>
  )
}

TopBar.propTypes = {
esikkala's avatar
esikkala committed
  /**
   * Redux action for full text search results using the search field.
   */
  fetchFullTextResults: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Redux action for clearing the full text results.
   */
  clearResults: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Redux action for loading translations.
   */
  loadLocales: PropTypes.func.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Current locale as a two-letter code
   */
  currentLocale: PropTypes.string.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Available locales as an array of objects with two-letter codes as keys.
   */
  availableLocales: PropTypes.array.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Perspective config as an array of objects.
   */
  perspectives: PropTypes.array.isRequired,
  /**
   * Flag for checking if the screen is extra small.
   */
esikkala's avatar
esikkala committed
  xsScreen: PropTypes.bool.isRequired,
esikkala's avatar
esikkala committed
  /**
   * React Router's location object. The perspective links are highlighted based on this.
   */
  location: PropTypes.object.isRequired,
esikkala's avatar
esikkala committed
  /**
   * Root url of the application.
   */
  rootUrl: PropTypes.string.isRequired
esikkala's avatar
esikkala committed
export default TopBar