flex-template-web/src/components/Topbar/Topbar.js
2019-08-29 16:37:47 +03:00

335 lines
10 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, intlShape, injectIntl } from '../../util/reactIntl';
import pickBy from 'lodash/pickBy';
import classNames from 'classnames';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import { withViewport } from '../../util/contextHelpers';
import { parse, stringify } from '../../util/urlHelpers';
import { createResourceLocatorString, pathByRouteName } from '../../util/routes';
import { propTypes } from '../../util/types';
import {
Button,
Logo,
Modal,
ModalMissingInformation,
NamedLink,
TopbarDesktop,
TopbarMobileMenu,
} from '../../components';
import { TopbarSearchForm } from '../../forms';
import MenuIcon from './MenuIcon';
import SearchIcon from './SearchIcon';
import css from './Topbar.css';
const MAX_MOBILE_SCREEN_WIDTH = 768;
const redirectToURLWithModalState = (props, modalStateParam) => {
const { history, location } = props;
const { pathname, search, state } = location;
const searchString = `?${stringify({ [modalStateParam]: 'open', ...parse(search) })}`;
history.push(`${pathname}${searchString}`, state);
};
const redirectToURLWithoutModalState = (props, modalStateParam) => {
const { history, location } = props;
const { pathname, search, state } = location;
const queryParams = pickBy(parse(search), (v, k) => {
return k !== modalStateParam;
});
const stringified = stringify(queryParams);
const searchString = stringified ? `?${stringified}` : '';
history.push(`${pathname}${searchString}`, state);
};
const GenericError = props => {
const { show } = props;
const classes = classNames(css.genericError, {
[css.genericErrorVisible]: show,
});
return (
<div className={classes}>
<div className={css.genericErrorContent}>
<p className={css.genericErrorText}>
<FormattedMessage id="Topbar.genericError" />
</p>
</div>
</div>
);
};
const { bool } = PropTypes;
GenericError.propTypes = {
show: bool.isRequired,
};
class TopbarComponent extends Component {
constructor(props) {
super(props);
this.handleMobileMenuOpen = this.handleMobileMenuOpen.bind(this);
this.handleMobileMenuClose = this.handleMobileMenuClose.bind(this);
this.handleMobileSearchOpen = this.handleMobileSearchOpen.bind(this);
this.handleMobileSearchClose = this.handleMobileSearchClose.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleLogout = this.handleLogout.bind(this);
}
handleMobileMenuOpen() {
redirectToURLWithModalState(this.props, 'mobilemenu');
}
handleMobileMenuClose() {
redirectToURLWithoutModalState(this.props, 'mobilemenu');
}
handleMobileSearchOpen() {
redirectToURLWithModalState(this.props, 'mobilesearch');
}
handleMobileSearchClose() {
redirectToURLWithoutModalState(this.props, 'mobilesearch');
}
handleSubmit(values) {
const { currentSearchParams } = this.props;
const { search, selectedPlace } = values.location;
const { history } = this.props;
const { origin, bounds } = selectedPlace;
const originMaybe = config.sortSearchByDistance ? { origin } : {};
const searchParams = {
...currentSearchParams,
...originMaybe,
address: search,
bounds,
};
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, searchParams));
}
handleLogout() {
const { onLogout, history } = this.props;
onLogout().then(() => {
const path = pathByRouteName('LandingPage', routeConfiguration());
// In production we ensure that data is really lost,
// but in development mode we use stored values for debugging
if (config.dev) {
history.push(path);
} else if (typeof window !== 'undefined') {
window.location = path;
}
console.log('logged out'); // eslint-disable-line
});
}
render() {
const {
className,
rootClassName,
desktopClassName,
mobileRootClassName,
mobileClassName,
isAuthenticated,
authInProgress,
currentUser,
currentUserHasListings,
currentUserHasOrders,
currentPage,
notificationCount,
viewport,
intl,
location,
onManageDisableScrolling,
onResendVerificationEmail,
sendVerificationEmailInProgress,
sendVerificationEmailError,
showGenericError,
} = this.props;
const { mobilemenu, mobilesearch, address, origin, bounds } = parse(location.search, {
latlng: ['origin'],
latlngBounds: ['bounds'],
});
const notificationDot = notificationCount > 0 ? <div className={css.notificationDot} /> : null;
const isMobileLayout = viewport.width < MAX_MOBILE_SCREEN_WIDTH;
const isMobileMenuOpen = isMobileLayout && mobilemenu === 'open';
const isMobileSearchOpen = isMobileLayout && mobilesearch === 'open';
const mobileMenu = (
<TopbarMobileMenu
isAuthenticated={isAuthenticated}
currentUserHasListings={currentUserHasListings}
currentUser={currentUser}
onLogout={this.handleLogout}
notificationCount={notificationCount}
currentPage={currentPage}
/>
);
// Only render current search if full place object is available in the URL params
const locationFieldsPresent = config.sortSearchByDistance
? address && origin && bounds
: address && bounds;
const initialSearchFormValues = {
location: locationFieldsPresent
? {
search: address,
selectedPlace: { address, origin, bounds },
}
: null,
};
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
<div className={classNames(mobileRootClassName || css.container, mobileClassName)}>
<Button
rootClassName={css.menu}
onClick={this.handleMobileMenuOpen}
title={intl.formatMessage({ id: 'Topbar.menuIcon' })}
>
<MenuIcon className={css.menuIcon} />
{notificationDot}
</Button>
<NamedLink
className={css.home}
name="LandingPage"
title={intl.formatMessage({ id: 'Topbar.logoIcon' })}
>
<Logo format="mobile" />
</NamedLink>
<Button
rootClassName={css.searchMenu}
onClick={this.handleMobileSearchOpen}
title={intl.formatMessage({ id: 'Topbar.searchIcon' })}
>
<SearchIcon className={css.searchMenuIcon} />
</Button>
</div>
<div className={css.desktop}>
<TopbarDesktop
className={desktopClassName}
currentUserHasListings={currentUserHasListings}
currentUser={currentUser}
currentPage={currentPage}
initialSearchFormValues={initialSearchFormValues}
intl={intl}
isAuthenticated={isAuthenticated}
notificationCount={notificationCount}
onLogout={this.handleLogout}
onSearchSubmit={this.handleSubmit}
/>
</div>
<Modal
id="TopbarMobileMenu"
isOpen={isMobileMenuOpen}
onClose={this.handleMobileMenuClose}
onManageDisableScrolling={onManageDisableScrolling}
>
{authInProgress ? null : mobileMenu}
</Modal>
<Modal
id="TopbarMobileSearch"
containerClassName={css.modalContainer}
isOpen={isMobileSearchOpen}
onClose={this.handleMobileSearchClose}
onManageDisableScrolling={onManageDisableScrolling}
>
<div className={css.searchContainer}>
<TopbarSearchForm
onSubmit={this.handleSubmit}
initialValues={initialSearchFormValues}
isMobile
/>
<p className={css.mobileHelp}>
<FormattedMessage id="Topbar.mobileSearchHelp" />
</p>
</div>
</Modal>
<ModalMissingInformation
id="MissingInformationReminder"
containerClassName={css.missingInformationModal}
currentUser={currentUser}
currentUserHasListings={currentUserHasListings}
currentUserHasOrders={currentUserHasOrders}
location={location}
onManageDisableScrolling={onManageDisableScrolling}
onResendVerificationEmail={onResendVerificationEmail}
sendVerificationEmailInProgress={sendVerificationEmailInProgress}
sendVerificationEmailError={sendVerificationEmailError}
/>
<GenericError show={showGenericError} />
</div>
);
}
}
TopbarComponent.defaultProps = {
className: null,
rootClassName: null,
desktopClassName: null,
mobileRootClassName: null,
mobileClassName: null,
notificationCount: 0,
currentUser: null,
currentUserHasOrders: null,
currentPage: null,
sendVerificationEmailError: null,
};
const { func, number, shape, string } = PropTypes;
TopbarComponent.propTypes = {
className: string,
rootClassName: string,
desktopClassName: string,
mobileRootClassName: string,
mobileClassName: string,
isAuthenticated: bool.isRequired,
authInProgress: bool.isRequired,
currentUser: propTypes.currentUser,
currentUserHasListings: bool.isRequired,
currentUserHasOrders: bool,
currentPage: string,
notificationCount: number,
onLogout: func.isRequired,
onManageDisableScrolling: func.isRequired,
onResendVerificationEmail: func.isRequired,
sendVerificationEmailInProgress: bool.isRequired,
sendVerificationEmailError: propTypes.error,
showGenericError: bool.isRequired,
// These are passed from Page to keep Topbar rendering aware of location changes
history: shape({
push: func.isRequired,
}).isRequired,
location: shape({
search: string.isRequired,
}).isRequired,
// from withViewport
viewport: shape({
width: number.isRequired,
height: number.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const Topbar = compose(
withViewport,
injectIntl
)(TopbarComponent);
Topbar.displayName = 'Topbar';
export default Topbar;