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' import Typography from '@material-ui/core/Typography' import MenuItem from '@material-ui/core/MenuItem' import Menu from '@material-ui/core/Menu' 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' import TopBarSearchField from './TopBarSearchField' import TopBarInfoButton from './TopBarInfoButton' import TopBarLanguageButton from './TopBarLanguageButton' import Divider from '@material-ui/core/Divider' import { has } from 'lodash' import secoLogo from '../../img/logos/seco-logo-48x50.png' const useStyles = makeStyles(theme => ({ grow: { flexGrow: 1 }, topBarToolbar: props => ({ minHeight: props.layoutConfig.topBar.reducedHeight, [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: { minHeight: props.layoutConfig.topBar.defaultHeight }, paddingLeft: theme.spacing(1.5), paddingRight: theme.spacing(1.5) }), sectionDesktop: props => ({ display: 'none', [theme.breakpoints.up(props.layoutConfig.topBar.mobileMenuBreakpoint)]: { display: 'flex' } }), link: { textDecoration: 'none' }, sectionMobile: props => ({ display: 'flex', [theme.breakpoints.up(props.layoutConfig.topBar.mobileMenuBreakpoint)]: { display: 'none' } }), appBarButton: { whiteSpace: 'nowrap', color: 'white !important', border: `1px solid ${theme.palette.primary.main}` }, appBarButtonActive: { border: '1px solid white' }, appBarDivider: { marginLeft: theme.spacing(1), marginRight: theme.spacing(1), borderLeft: '2px solid white' }, secoLogo: props => ({ marginLeft: theme.spacing(1), [theme.breakpoints.down(props.layoutConfig.topBar.mobileMenuBreakpoint)]: { display: 'none' } }), secoLogoImage: props => ({ height: 32, [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: { height: 50 } }), mainLogo: props => ({ height: 23, [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: { height: 40 }, marginRight: theme.spacing(1) }), mainLogoButtonRoot: { paddingLeft: 0, [theme.breakpoints.down('xs')]: { minWidth: 48 } }, mainLogoButtonLabel: { justifyContent: 'left' }, mainLogoTypography: props => ({ // set color and background explicitly to keep Google Lighthouse happy color: '#fff', background: theme.palette.primary.main, whiteSpace: 'nowrap', textTransform: props.layoutConfig.topBar.logoTextTransform, [theme.breakpoints.down('sm')]: { fontSize: '1.5rem' }, ...(props.layoutConfig.topBar.hideLogoTextOnMobile && { [theme.breakpoints.down('xs')]: { display: 'none' } }) }), mobileMenuButton: { padding: 12 } })) /** * Responsive app bar with a search field, perspective links, info links and a language * selector. Based on Material-UI's App Bar component. */ const TopBar = props => { const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null) const isMobileMenuOpen = Boolean(mobileMoreAnchorEl) const { perspectives, currentLocale, availableLocales, rootUrl, layoutConfig } = props const { topBar } = layoutConfig const classes = useStyles(props) const handleMobileMenuOpen = event => setMobileMoreAnchorEl(event.currentTarget) const handleMobileMenuClose = () => setMobileMoreAnchorEl(null) const clientFSMode = props.location.pathname.indexOf('clientFS') !== -1 let showSearchField = true if (has(layoutConfig.topBar, 'showSearchField')) { showSearchField = layoutConfig.topBar.showSearchField } // https://material-ui.com/components/buttons/#third-party-routing-library const AdapterLink = React.forwardRef((props, ref) => <Link innerRef={ref} {...props} />) const AdapterNavLink = React.forwardRef((props, ref) => <NavLink innerRef={ref} {...props} />) const getInternalLink = perspective => { const searchMode = has(perspective, 'searchMode') ? perspective.searchMode : 'faceted-search' let link = null if (searchMode === 'dummy-internal') { link = `${props.rootUrl}${perspective.internalLink}` } if (searchMode !== 'dummy-internal') { link = `${props.rootUrl}/${perspective.id}/${searchMode}` } return link } const renderMobileMenuItem = perspective => { if (has(perspective, 'externalUrl')) { return ( <a className={classes.link} key={perspective.id} href={perspective.externalUrl} target='_blank' rel='noopener noreferrer' > <MenuItem> {perspective.label ? perspective.label.toUpperCase() : intl.get(`perspectives.${perspective.id}.label`).toUpperCase()} </MenuItem> </a> ) } else { return ( <MenuItem key={perspective.id} component={AdapterLink} to={getInternalLink(perspective)} onClick={handleMobileMenuClose} > {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()} </MenuItem> ) } } const renderDesktopTopMenuItem = perspective => { if (has(perspective, 'externalUrl')) { return ( <a className={classes.link} key={perspective.id} href={perspective.externalUrl} target='_blank' rel='noopener noreferrer' > <Button className={classes.appBarButton} > {perspective.label ? perspective.label : intl.get(`perspectives.${perspective.id}.label`).toUpperCase()} </Button> </a> ) } else { return ( <Button key={perspective.id} className={classes.appBarButton} component={AdapterNavLink} to={getInternalLink(perspective)} isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/${perspective.id}`)} activeClassName={classes.appBarButtonActive} > {intl.get(`perspectives.${perspective.id}.label`).toUpperCase()} </Button> ) } } const renderInfoItem = item => { let jsx if (item.externalLink) { jsx = ( <a className={classes.link} key={item.id} href={intl.get(`topBar.info.${item.translatedUrl}`)} target='_blank' rel='noopener noreferrer' > <MenuItem onClick={handleMobileMenuClose}> {intl.get(`topBar.info.${item.translatedText}`).toUpperCase()} </MenuItem> </a> ) } else { jsx = ( <MenuItem key={item.id} component={AdapterLink} to={`${props.rootUrl}${item.internalLink}`} onClick={handleMobileMenuClose} > {intl.get(`topBar.info.${item.translatedText}`).toUpperCase()} </MenuItem> ) } return jsx } const renderMobileMenu = perspectives => { const { topBar } = props.layoutConfig const { infoDropdown } = topBar return ( <Menu anchorEl={mobileMoreAnchorEl} anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} open={isMobileMenuOpen} onClose={handleMobileMenuClose} > {perspectives.map(perspective => perspective.hideTopPerspectiveButton ? null : renderMobileMenuItem(perspective))} <Divider /> {renderMobileMenuItem({ id: 'feedback', externalUrl: props.layoutConfig.topBar.feedbackLink, label: intl.get('topBar.feedback') })} {infoDropdown.map(item => renderInfoItem(item))} {topBar.externalInstructions && renderMobileMenuItem({ id: 'instructions', externalUrl: intl.get('topBar.instructionsUrl'), label: intl.get('topBar.instructions') })} {!topBar.externalInstructions && <MenuItem key='instructions' component={AdapterLink} to={`${props.rootUrl}/instructions`} onClick={handleMobileMenuClose} > {intl.get('topBar.instructions').toUpperCase()} </MenuItem>} </Menu> ) } return ( <div className={classes.root}> {/* Add an empty Typography element to ensure that that the MuiTypography class is loaded for any lower level components that use MuiTypography class only in translation files */} <Typography /> <AppBar position='static'> <Toolbar className={classes.topBarToolbar}> <Button component={AdapterLink} to='/' classes={{ root: classes.mainLogoButtonRoot, label: classes.mainLogoButtonLabel }} onClick={() => clientFSMode ? props.clientFSClearResults() : null} > {topBar.logoImage && <img className={classes.mainLogo} src={topBar.logoImage} alt={`${intl.get('appTitle.short')} logo`} />} <Typography className={classes.mainLogoTypography} variant='h5'> {props.xsScreen ? intl.get('appTitle.mobile') : intl.get('appTitle.short')} </Typography> </Button> {showSearchField && <TopBarSearchField fetchFullTextResults={props.fetchFullTextResults} clearResults={props.clearResults} xsScreen={props.xsScreen} rootUrl={rootUrl} />} <div className={classes.grow} /> <div className={classes.sectionDesktop}> {perspectives.map((perspective, index) => perspective.hideTopPerspectiveButton ? null : renderDesktopTopMenuItem(perspective, index))} <div className={classes.appBarDivider} /> {renderDesktopTopMenuItem({ id: 'feedback', externalUrl: props.layoutConfig.topBar.feedbackLink, label: intl.get('topBar.feedback') })} <TopBarInfoButton rootUrl={props.rootUrl} layoutConfig={layoutConfig} /> {topBar.externalInstructions && renderDesktopTopMenuItem({ id: 'instructions', externalUrl: intl.get('topBar.instructionsUrl'), label: intl.get('topBar.instructions') })} {!topBar.externalInstructions && <Button className={classes.appBarButton} component={AdapterNavLink} to={`${props.rootUrl}/instructions`} isActive={(match, location) => location.pathname.startsWith(`${props.rootUrl}/instructions`)} activeClassName={classes.appBarButtonActive} > {intl.get('topBar.instructions')} </Button>} {props.layoutConfig.topBar.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 aria-label='link to Semantic Computing research group homepage'> <img className={classes.secoLogoImage} src={secoLogo} alt='Semantic Computing research group logo' /> </Button> </a> <div className={classes.sectionMobile}> {props.layoutConfig.topBar.showLanguageButton && <TopBarLanguageButton currentLocale={currentLocale} availableLocales={availableLocales} loadLocales={props.loadLocales} location={props.location} />} <IconButton aria-label='display more actions' color='inherit' className={classes.mobileMenuButton} onClick={handleMobileMenuOpen} > <MoreIcon /> </IconButton> </div> </Toolbar> </AppBar> {renderMobileMenu(perspectives)} </div> ) } TopBar.propTypes = { /** * Redux action for full text search results using the search field. */ fetchFullTextResults: PropTypes.func.isRequired, /** * Redux action for clearing the full text results. */ clearResults: PropTypes.func.isRequired, /** * Redux action for loading translations. */ loadLocales: PropTypes.func.isRequired, /** * Current locale as a two-letter code */ currentLocale: PropTypes.string.isRequired, /** * Available locales as an array of objects with two-letter codes as keys. */ availableLocales: PropTypes.array.isRequired, /** * Perspective config as an array of objects. */ perspectives: PropTypes.array.isRequired, /** * Flag for checking if the screen is extra small. */ xsScreen: PropTypes.bool.isRequired, /** * React Router's location object. The perspective links are highlighted based on this. */ location: PropTypes.object.isRequired, /** * Root url of the application. */ rootUrl: PropTypes.string.isRequired, layoutConfig: PropTypes.object.isRequired } export default TopBar