import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; import { debounce, isEqual, unionWith } from 'lodash'; import classNames from 'classnames'; import config from '../../config'; import { withFlattenedRoutes } from '../../util/contextHelpers'; import { googleLatLngToSDKLatLng, googleBoundsToSDKBounds } from '../../util/googleMaps'; import { createResourceLocatorString } from '../../util/routes'; import { parse, stringify } from '../../util/urlHelpers'; import * as propTypes from '../../util/propTypes'; import { getListingsById } from '../../ducks/marketplaceData.duck'; import { logout, authenticationInProgress } from '../../ducks/Auth.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck'; import { SearchMap, ModalInMobile, PageLayout, SearchResultsPanel, Topbar } from '../../components'; import { searchListings, searchMapListings } from './SearchPage.duck'; import MapIcon from './MapIcon'; import css from './SearchPage.css'; // Pagination page size might need to be dynamic on responsive page layouts // Current design has max 3 columns 12 is divisible by 2 and 3 // So, there's enough cards to fill all columns on full pagination pages const RESULT_PAGE_SIZE = 12; const SHARETRIBE_API_MAX_PAGE_SIZE = 100; const MAX_SEARCH_RESULT_PAGES_ON_MAP = 5; // 100 * 5 = 500 listings are shown on a map. const DEBOUNCE_MAP_BOUNDS_CHANGE = 500; // bounds_change event is fired too often while dragging const MODAL_BREAKPOINT = 768; /* Search is in modal on mobile layout */ const pickSearchParamsOnly = params => { const { address, origin, bounds } = params || {}; return { address, origin, bounds }; }; export class SearchPageComponent extends Component { constructor(props) { super(props); this.state = { isSearchMapOpenOnMobile: false, }; // Initiating map creates 'bounds_changes' event // we listen to that event to make new searches // So, if the search comes from location search input, // we need to by pass 2nd search created by initial 'bounds_changes' event this.useLocationSearchBounds = true; this.modalOpenedBoundsChange = false; this.onBoundsChanged = debounce(this.onBoundsChanged.bind(this), DEBOUNCE_MAP_BOUNDS_CHANGE); this.fetchMoreListingsToMap = this.fetchMoreListingsToMap.bind(this); } componentDidMount() { this.fetchMoreListingsToMap(this.props.location); } componentWillReceiveProps(nextProps) { if (!isEqual(this.props.location, nextProps.location)) { this.fetchMoreListingsToMap(nextProps.location); // If no boundsChanged url parameter is given, this is original location search const { boundsChanged } = parse(location.search, { latlng: ['origin'], latlngBounds: ['bounds'], }); if (!boundsChanged) { this.useLocationSearchBounds = true; } } } onBoundsChanged(googleMap) { const { flattenedRoutes, history, location } = this.props; const { address, country, boundsChanged } = parse(location.search, { latlng: ['origin'], latlngBounds: ['bounds'], }); // If boundsChanged url param is given (and we have not just opened mobile map modal) // or original location search is rendered once, // we start to react to 'bounds_changed' event by generating new searches if ((boundsChanged && !this.modalOpenedBoundsChange) || !this.useLocationSearchBounds) { const viewportBounds = googleMap.getBounds(); const bounds = googleBoundsToSDKBounds(viewportBounds); const origin = googleLatLngToSDKLatLng(viewportBounds.getCenter()); const searchParams = { address, origin, bounds, country, boundsChanged: true }; history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams)); } else { this.useLocationSearchBounds = false; this.modalOpenedBoundsChange = false; } } fetchMoreListingsToMap(location) { const { onSearchMapListings } = this.props; const searchInURL = parse(location.search, { latlng: ['origin'], latlngBounds: ['bounds'], }); const perPage = SHARETRIBE_API_MAX_PAGE_SIZE; const page = 1; const searchParamsForMapResults = { ...searchInURL, page, perPage }; // Search more listings for map onSearchMapListings(searchParamsForMapResults) .then(response => { const hasNextPage = page < response.data.meta.totalPages && page < MAX_SEARCH_RESULT_PAGES_ON_MAP; if (hasNextPage) { onSearchMapListings({ ...searchParamsForMapResults, page: page + 1 }); } }) .catch(error => { // In case of error, stop recursive loop and report error. // eslint-disable-next-line no-console console.error(`An error (${error} occured while trying to retrieve map listings`); }); } render() { const { authInfoError, authInProgress, currentUser, currentUserHasListings, history, isAuthenticated, listings, location, logoutError, mapListings, notificationCount, onLogout, onManageDisableScrolling, pagination, scrollingDisabled, searchInProgress, searchListingsError, searchParams, tab, } = this.props; // eslint-disable-next-line no-unused-vars const { boundsChanged, page, ...searchInURL } = parse(location.search, { latlng: ['origin'], latlngBounds: ['bounds'], }); // Page transition might initially use values from previous search const searchParamsInURL = stringify(pickSearchParamsOnly(searchInURL)); const searchParamsInProps = stringify(pickSearchParamsOnly(searchParams)); const searchParamsMatch = searchParamsInURL === searchParamsInProps; const { address, bounds, origin } = searchInURL || {}; const hasPaginationInfo = !!pagination && pagination.totalItems != null; const totalItems = searchParamsMatch && hasPaginationInfo ? pagination.totalItems : 0; const listingsAreLoaded = !searchInProgress && searchParamsMatch && hasPaginationInfo; const searchError = (