mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-31 02:26:50 +10:00
Merge pull request #307 from sharetribe/searchpage-desktop
Searchpage desktop
This commit is contained in:
commit
861be091b2
21 changed files with 849 additions and 154 deletions
82
src/components/MapPriceMarker/MapPriceMarker.css
Normal file
82
src/components/MapPriceMarker/MapPriceMarker.css
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
|
||||
transform: translate( -50%, -33px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.priceLabel {
|
||||
/**
|
||||
* Since caret is absolutely positioned,
|
||||
* label must have relative to be included to the same rendering layer
|
||||
*/
|
||||
position: relative;
|
||||
|
||||
/* Font */
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
color: var(--matterColor);
|
||||
|
||||
background-color: var(--matterColorLight);
|
||||
|
||||
/* Borders */
|
||||
border-style: solid;
|
||||
border-color: var(--matterColorNegative);
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--boxShadowPopupLight);
|
||||
|
||||
/* Dimensions */
|
||||
padding: 10px 10px 8px 10px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--marketplaceColor);
|
||||
}
|
||||
|
||||
/* Overwrite dimensions from font styles */
|
||||
@media (--desktopViewport) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.caretShadow {
|
||||
/* Caret / arrow dimensions and position */
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
transform: rotate(45deg);
|
||||
|
||||
/* Caret should have same box-shadow as label */
|
||||
box-shadow: var(--boxShadowPopupLight);
|
||||
}
|
||||
|
||||
.caret {
|
||||
|
||||
/* Caret / arrow dimensions and position */
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
transform: rotate(45deg);
|
||||
|
||||
/* Caret should have same bg-color and border as label */
|
||||
background-color: var(--matterColorLight);
|
||||
border-right-style: solid;
|
||||
border-right-color: var(--matterColorNegative);
|
||||
border-right-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--matterColorNegative);
|
||||
border-bottom-width: 1px;
|
||||
|
||||
}
|
||||
63
src/components/MapPriceMarker/MapPriceMarker.js
Normal file
63
src/components/MapPriceMarker/MapPriceMarker.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* MapPriceMarker is build on top of google.maps.OverlayView so that we can actually render HTML
|
||||
* to the map. Price label's width varies and therefore we can't use default fixed-size markers.
|
||||
*/
|
||||
import React, { PropTypes } from 'react';
|
||||
import { intlShape, injectIntl } from 'react-intl';
|
||||
import config from '../../config';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { convertMoneyToNumber } from '../../util/currency';
|
||||
|
||||
import OverlayLayer from './OverlayLayer';
|
||||
import css from './MapPriceMarker.css';
|
||||
|
||||
const priceLabel = (price, currencyConfig, intl) => {
|
||||
if (price && price.currency === currencyConfig.currency) {
|
||||
const priceAsNumber = convertMoneyToNumber(price, currencyConfig.subUnitDivisor);
|
||||
return intl.formatNumber(priceAsNumber, currencyConfig);
|
||||
} else if (price) {
|
||||
return intl.formatMessage(
|
||||
{ id: 'MapPriceMarker.unsupportedPrice' },
|
||||
{ currency: price.currency }
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const MapPriceMarker = props => {
|
||||
const { intl, listing, map, onAddOverlay } = props;
|
||||
const { price, geolocation } = listing.attributes;
|
||||
const label = priceLabel(price, config.currencyConfig, intl);
|
||||
|
||||
return (
|
||||
<OverlayLayer
|
||||
id={listing.id.uuid}
|
||||
map={map}
|
||||
geolocation={geolocation}
|
||||
onAddOverlay={onAddOverlay}
|
||||
>
|
||||
<div className={css.root}>
|
||||
<div className={css.caretShadow} />
|
||||
<div className={css.priceLabel}>
|
||||
{label}
|
||||
</div>
|
||||
<div className={css.caret} />
|
||||
</div>
|
||||
</OverlayLayer>
|
||||
);
|
||||
};
|
||||
|
||||
MapPriceMarker.defaultProps = {
|
||||
map: null,
|
||||
};
|
||||
|
||||
const { func, object } = PropTypes;
|
||||
|
||||
MapPriceMarker.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
listing: propTypes.listing.isRequired,
|
||||
map: object,
|
||||
onAddOverlay: func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(MapPriceMarker);
|
||||
124
src/components/MapPriceMarker/OverlayLayer.js
Normal file
124
src/components/MapPriceMarker/OverlayLayer.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { Component, PropTypes } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
class OverlayLayer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAdd = this.onAdd.bind(this);
|
||||
this.onRemove = this.onRemove.bind(this);
|
||||
this.draw = this.draw.bind(this);
|
||||
this.overlayContainer = null;
|
||||
|
||||
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#OverlayView
|
||||
const overlayView = new window.google.maps.OverlayView();
|
||||
|
||||
// As stated by Google these three methods must implemented: onAdd(), draw(), and onRemove().
|
||||
overlayView.onAdd = this.onAdd;
|
||||
overlayView.draw = this.draw;
|
||||
overlayView.onRemove = this.onRemove;
|
||||
|
||||
// You must call setMap() with a valid Map object to trigger the call to
|
||||
// the onAdd() method and setMap(null) in order to trigger the onRemove() method.
|
||||
overlayView.setMap(props.map);
|
||||
|
||||
this.state = { overlayView };
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const onAddOverlay = this.props.onAddOverlay;
|
||||
if (onAddOverlay) {
|
||||
// If onAddOverlay is saving anything to parent component's state,
|
||||
// it must be called here to avoid warnings.
|
||||
onAddOverlay(this.state.overlayView);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const overlayView = this.state.overlayView;
|
||||
if (overlayView) {
|
||||
overlayView.setMap(null);
|
||||
overlayView.onAdd = null;
|
||||
overlayView.draw = null;
|
||||
overlayView.onRemove = null;
|
||||
}
|
||||
}
|
||||
|
||||
onAdd() {
|
||||
// Create container for this custom "marker"
|
||||
this.overlayContainer = document.createElement(`div`);
|
||||
this.overlayContainer.style.position = `absolute`;
|
||||
this.overlayContainer.dataset.overlayId = this.props.id;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
// Remove container from parent node and tell React about it too
|
||||
this.overlayContainer.parentNode.removeChild(this.overlayContainer);
|
||||
unmountComponentAtNode(this.overlayContainer);
|
||||
}
|
||||
|
||||
draw() {
|
||||
const overlayView = this.state.overlayView;
|
||||
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#MapCanvasProjection
|
||||
const overlayProjection = overlayView.getProjection();
|
||||
|
||||
// render children to overlayContainer.
|
||||
if (this.overlayContainer) {
|
||||
this.overlayContainer.innerHTML = '';
|
||||
render(this.props.children, this.overlayContainer);
|
||||
}
|
||||
|
||||
// Add the element to the "overlayMouseTarget" pane.
|
||||
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#MapPanes
|
||||
const mapPanes = this.state.overlayView.getPanes();
|
||||
|
||||
if (mapPanes && this.overlayContainer) {
|
||||
const geolocation = this.props.geolocation;
|
||||
const googleLatLng = new window.google.maps.LatLng({
|
||||
lat: geolocation.lat,
|
||||
lng: geolocation.lng,
|
||||
});
|
||||
const point = overlayProjection.fromLatLngToDivPixel(googleLatLng);
|
||||
|
||||
if (point) {
|
||||
this.overlayContainer.style.left = `${point.x}px`;
|
||||
this.overlayContainer.style.top = `${point.y}px`;
|
||||
this.overlayContainer.style.height = `0`;
|
||||
}
|
||||
|
||||
// overlayMouseTarget might not be a correct pane - overlayLayer is used by some services
|
||||
mapPanes.overlayMouseTarget.appendChild(this.overlayContainer);
|
||||
|
||||
window.google.maps.event.addDomListener(this.overlayContainer, 'click', event => {
|
||||
// TODO: Click handler needs to open info window at some point
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`OverlayLayer (${event.target.closest('[data-overlay-id]').dataset.overlayId}) clicked.`
|
||||
);
|
||||
window.google.maps.event.trigger(self, 'click');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
OverlayLayer.defaultProps = { children: null, map: null, onAddOverlay: null };
|
||||
|
||||
const { func, node, number, object, shape, string } = PropTypes;
|
||||
|
||||
OverlayLayer.propTypes = {
|
||||
children: node,
|
||||
geolocation: shape({ lat: number, lng: number }).isRequired,
|
||||
id: string.isRequired,
|
||||
map: object,
|
||||
onAddOverlay: func,
|
||||
};
|
||||
|
||||
export default OverlayLayer;
|
||||
|
|
@ -38,7 +38,9 @@ class MenuLabel extends Component {
|
|||
const { children, className, rootClassName } = this.props;
|
||||
|
||||
const rootClass = rootClassName || css.root;
|
||||
const classes = classNames(rootClass, className, { [css.clickedWithMouse]: this.state.clickedWithMouse });
|
||||
const classes = classNames(rootClass, className, {
|
||||
[css.clickedWithMouse]: this.state.clickedWithMouse,
|
||||
});
|
||||
|
||||
return (
|
||||
<button className={classes} onClick={this.onClick} onBlur={this.onBlur}>
|
||||
|
|
@ -49,7 +51,12 @@ class MenuLabel extends Component {
|
|||
}
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
MenuLabel.defaultProps = { className: null, isOpen: false, onToggleActive: null, rootClassName: '' };
|
||||
MenuLabel.defaultProps = {
|
||||
className: null,
|
||||
isOpen: false,
|
||||
onToggleActive: null,
|
||||
rootClassName: '',
|
||||
};
|
||||
|
||||
const { func, node, string } = PropTypes;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
/* Content is visible as modal layer */
|
||||
.isOpen {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
|||
4
src/components/SearchMap/SearchMap.css
Normal file
4
src/components/SearchMap/SearchMap.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
129
src/components/SearchMap/SearchMap.js
Normal file
129
src/components/SearchMap/SearchMap.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { isEqualWith, sortBy } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { types as sdkTypes } from '../../util/sdkLoader';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { MapPriceMarker } from '../../components';
|
||||
|
||||
import css from './SearchMap.css';
|
||||
|
||||
const fitMapToBounds = (map, bounds) => {
|
||||
const { ne, sw } = bounds || {};
|
||||
// map bounds as string literal for google.maps
|
||||
const mapBounds = bounds ? { north: ne.lat, east: ne.lng, south: sw.lat, west: sw.lng } : null;
|
||||
|
||||
if (mapBounds) {
|
||||
map.fitBounds(mapBounds);
|
||||
}
|
||||
};
|
||||
|
||||
// Compare listing arrays (if listings are the same, there's no need to rerender google maps).
|
||||
const hasSameListings = (prevListings, nextListings) => {
|
||||
return isEqualWith(sortBy(prevListings, l => l.id.uuid), sortBy(nextListings, l => l.id.uuid));
|
||||
};
|
||||
|
||||
class SearchMap extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.map = null;
|
||||
this.state = { overlays: [] };
|
||||
this.addOverlay = this.addOverlay.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mapsLibLoaded = window.google && window.google.maps;
|
||||
if (!mapsLibLoaded) {
|
||||
throw new Error('Google Maps API must be loaded for the SearchMap component');
|
||||
}
|
||||
|
||||
const { bounds, center, zoom } = this.props;
|
||||
const centerLocation = { lat: center.lat, lng: center.lng };
|
||||
const mapOptions = {
|
||||
center: centerLocation,
|
||||
zoom,
|
||||
|
||||
// Disable map type (ie. Satellite etc.)
|
||||
mapTypeControl: false,
|
||||
|
||||
// Disable zooming by scrolling
|
||||
scrollwheel: false,
|
||||
};
|
||||
this.map = new window.google.maps.Map(this.el, mapOptions);
|
||||
|
||||
// If bounds are given, use it (defaults to center & zoom).
|
||||
fitMapToBounds(this.map, bounds);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { bounds, listings } = nextProps;
|
||||
|
||||
fitMapToBounds(this.map, bounds);
|
||||
|
||||
if (!hasSameListings(listings, nextProps.listings)) {
|
||||
// Clear markers from map
|
||||
this.state.overlays.forEach(o => o.setMap(null));
|
||||
}
|
||||
}
|
||||
|
||||
addOverlay(overlay) {
|
||||
this.setState(prevState => {
|
||||
// Track map overlays, in case we need to clear them from map
|
||||
const overlays = prevState.overlays.concat([overlay]);
|
||||
return { overlays };
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, rootClassName, listings } = this.props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
// Add listing markers to map if listings prop is passed
|
||||
// By reversing the order, we can show the nearest price labels on top of more distant ones.
|
||||
const currentMarkers = listings
|
||||
.map(l => {
|
||||
return (
|
||||
<MapPriceMarker
|
||||
key={l.id.uuid}
|
||||
map={this.map}
|
||||
listing={l}
|
||||
onAddOverlay={this.addOverlay}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
ref={el => {
|
||||
this.el = el;
|
||||
}}
|
||||
>
|
||||
{currentMarkers}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchMap.defaultProps = {
|
||||
className: '',
|
||||
rootClassName: null,
|
||||
bounds: null,
|
||||
center: new sdkTypes.LatLng(0, 0),
|
||||
listings: [],
|
||||
zoom: 11,
|
||||
};
|
||||
|
||||
const { arrayOf, number, string } = PropTypes;
|
||||
|
||||
SearchMap.propTypes = {
|
||||
bounds: propTypes.latlngBounds,
|
||||
center: propTypes.latlng,
|
||||
className: string,
|
||||
listings: arrayOf(propTypes.listing),
|
||||
rootClassName: string,
|
||||
zoom: number,
|
||||
};
|
||||
|
||||
export default SearchMap;
|
||||
|
|
@ -7,10 +7,69 @@
|
|||
|
||||
.listingCards {
|
||||
padding: 0 24px 24px 24px;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.listingCard {
|
||||
margin-bottom: 36px;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-basis: calc(100%);
|
||||
/* ListingCard's minimum width on desktop */
|
||||
min-width: 264px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
/**
|
||||
* resultPanelWidthRatio = 0.6 aka 60%
|
||||
* ((columnCount * ListingCardWidth + gutter * (columnCount + 1)) / resultPanelWidthRatio = 1000
|
||||
* min-width: (2 * 264 + 24 * 3) / 0.6 = 1000
|
||||
* flex-basis: calc((100%/columnCount) - (guttersBetweenColumns * gutterWidth / columnCount))
|
||||
*/
|
||||
flex-basis: calc(50% - 12px);
|
||||
margin-right: 24px;
|
||||
min-width: 264px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1480px) {
|
||||
/* (3*264 + 24 * 4) / 0.6 = 1420 */
|
||||
|
||||
flex-basis: calc(33.33% - 16px);
|
||||
margin-right: 24px;
|
||||
min-width: 264px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1960px) {
|
||||
/* (4*264 + 24 * 5) / 0.6 = 1960 */
|
||||
|
||||
flex-basis: calc(25% - 18px);
|
||||
margin-right: 24px;
|
||||
min-width: 264px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove margin-right from listingCards on the last column */
|
||||
.listingCard:nth-of-type(2n) {
|
||||
@media screen and (min-width: 1000px) and (max-width: 1479px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.listingCard:nth-of-type(3n) {
|
||||
@media screen and (min-width: 1480px) and (max-width: 1959px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.listingCard:nth-of-type(4n) {
|
||||
@media screen and (min-width: 1960px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import LocationAutocompleteInput, {
|
|||
} from './LocationAutocompleteInput/LocationAutocompleteInput';
|
||||
import Map from './Map/Map';
|
||||
import MapPanel from './MapPanel/MapPanel';
|
||||
import MapPriceMarker from './MapPriceMarker/MapPriceMarker';
|
||||
import Menu from './Menu/Menu';
|
||||
import MenuContent from './MenuContent/MenuContent';
|
||||
import MenuItem from './MenuItem/MenuItem';
|
||||
|
|
@ -41,6 +42,7 @@ import ResponsiveImage from './ResponsiveImage/ResponsiveImage';
|
|||
import RoutesProvider from './RoutesProvider/RoutesProvider';
|
||||
import SaleDetailsPanel from './SaleDetailsPanel/SaleDetailsPanel';
|
||||
import SearchIcon from './SearchIcon/SearchIcon';
|
||||
import SearchMap from './SearchMap/SearchMap';
|
||||
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
|
||||
import SelectField from './SelectField/SelectField';
|
||||
import StripeBankAccountTokenInputField
|
||||
|
|
@ -81,6 +83,7 @@ export {
|
|||
LocationAutocompleteInputField,
|
||||
Map,
|
||||
MapPanel,
|
||||
MapPriceMarker,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
|
|
@ -100,6 +103,7 @@ export {
|
|||
RoutesProvider,
|
||||
SaleDetailsPanel,
|
||||
SearchIcon,
|
||||
SearchMap,
|
||||
SearchResultsPanel,
|
||||
SecondaryButton,
|
||||
SelectField,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ export class EditListingPhotosFormComponent extends Component {
|
|||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.setState({ imageUploadRequested: true });
|
||||
this.props.onImageUpload({ id: `${file.name}_${Date.now()}`, file })
|
||||
this.props
|
||||
.onImageUpload({ id: `${file.name}_${Date.now()}`, file })
|
||||
.then(() => {
|
||||
this.setState({ imageUploadRequested: false });
|
||||
})
|
||||
|
|
@ -164,11 +165,7 @@ export class EditListingPhotosFormComponent extends Component {
|
|||
{createListingFailed}
|
||||
{showListingFailed}
|
||||
|
||||
<Button
|
||||
className={css.submitButton}
|
||||
type="submit"
|
||||
disabled={disableForm}
|
||||
>
|
||||
<Button className={css.submitButton} type="submit" disabled={disableForm}>
|
||||
{saveActionMsg}
|
||||
</Button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ export const EditProfilePageComponent = props => {
|
|||
} = props;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title={`Edit profile page with display name: ${params.displayName}`}>
|
||||
<PageLayout
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
title={`Edit profile page with display name: ${params.displayName}`}
|
||||
>
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class NotFoundPageComponent extends Component {
|
|||
</PageLayout>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
NotFoundPageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ export const PasswordForgottenPageComponent = props => {
|
|||
} = props;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title="Request new password">
|
||||
<PageLayout
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
title="Request new password"
|
||||
>
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ export const ProfilePageComponent = props => {
|
|||
} = props;
|
||||
|
||||
return (
|
||||
<PageLayout authInfoError={authInfoError} logoutError={logoutError} title={`Profile page with display name: ${params.displayName}`}>
|
||||
<PageLayout
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
title={`Profile page with display name: ${params.displayName}`}
|
||||
>
|
||||
<Topbar
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,44 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
:root {
|
||||
--containerHeight: calc(100vh - var(--topbarHeightDesktop));
|
||||
}
|
||||
|
||||
.topbar {
|
||||
@media (--desktopViewport) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
/* We need to raise Topbar above .container */
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
/* Layout */
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
@media (--desktopViewport) {
|
||||
position: relative;
|
||||
padding-top: var(--topbarHeightDesktop);
|
||||
min-height: var(--containerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
.searchResultContainer {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (--desktopViewport) {
|
||||
/**
|
||||
* .container is using flexbox,
|
||||
* This specifies that searchResultContainer is taking 60% from the viewport width
|
||||
*/
|
||||
flex-basis: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.searchResultSummary {
|
||||
|
|
@ -17,6 +51,7 @@
|
|||
}
|
||||
|
||||
.searchString {
|
||||
/* Search string should not break on white spaces - i.e. line-break should happen before. */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
@ -29,3 +64,33 @@
|
|||
.searchListingsPanel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mapPanel {
|
||||
display: none;
|
||||
|
||||
@media (--desktopViewport) {
|
||||
/**
|
||||
* .container is using flexbox,
|
||||
* This specifies that mapPanel is taking 40% from the viewport width
|
||||
*/
|
||||
flex-basis: 40%;
|
||||
|
||||
/* Own layout settings */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map {
|
||||
@media (--desktopViewport) {
|
||||
/* Map is fixed so that it doesn't scroll along search results */
|
||||
position: fixed;
|
||||
top: var(--topbarHeightDesktop);
|
||||
right: 0;
|
||||
|
||||
/* Fixed content needs width relative to viewport */
|
||||
width: 40vw;
|
||||
height: var(--containerHeight);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export const SEARCH_LISTINGS_REQUEST = 'app/SearchPage/SEARCH_LISTINGS_REQUEST';
|
|||
export const SEARCH_LISTINGS_SUCCESS = 'app/SearchPage/SEARCH_LISTINGS_SUCCESS';
|
||||
export const SEARCH_LISTINGS_ERROR = 'app/SearchPage/SEARCH_LISTINGS_ERROR';
|
||||
|
||||
export const SEARCH_MAP_LISTINGS_REQUEST = 'app/SearchPage/SEARCH_MAP_LISTINGS_REQUEST';
|
||||
export const SEARCH_MAP_LISTINGS_SUCCESS = 'app/SearchPage/SEARCH_MAP_LISTINGS_SUCCESS';
|
||||
export const SEARCH_MAP_LISTINGS_ERROR = 'app/SearchPage/SEARCH_MAP_LISTINGS_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -14,6 +18,8 @@ const initialState = {
|
|||
searchInProgress: false,
|
||||
searchListingsError: null,
|
||||
currentPageResultIds: [],
|
||||
searchMapListingIds: [],
|
||||
searchMapListingsError: null,
|
||||
};
|
||||
|
||||
const resultIds = data => data.data.map(l => l.id);
|
||||
|
|
@ -40,6 +46,23 @@ const listingPageReducer = (state = initialState, action = {}) => {
|
|||
// eslint-disable-next-line no-console
|
||||
console.error(payload);
|
||||
return { ...state, searchInProgress: false, searchListingsError: payload };
|
||||
|
||||
case SEARCH_MAP_LISTINGS_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
//searchMapListingIds: [],
|
||||
searchMapListingsError: null,
|
||||
};
|
||||
case SEARCH_MAP_LISTINGS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
searchMapListingIds: state.searchMapListingIds.concat(resultIds(payload.data)),
|
||||
};
|
||||
case SEARCH_MAP_LISTINGS_ERROR:
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(payload);
|
||||
return { ...state, searchMapListingsError: payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -65,6 +88,19 @@ export const searchListingsError = e => ({
|
|||
payload: e,
|
||||
});
|
||||
|
||||
export const searchMapListingsRequest = () => ({ type: SEARCH_MAP_LISTINGS_REQUEST });
|
||||
|
||||
export const searchMapListingsSuccess = response => ({
|
||||
type: SEARCH_MAP_LISTINGS_SUCCESS,
|
||||
payload: { data: response.data },
|
||||
});
|
||||
|
||||
export const searchMapListingsError = e => ({
|
||||
type: SEARCH_MAP_LISTINGS_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
export const searchListings = searchParams =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(searchListingsRequest(searchParams));
|
||||
|
|
@ -87,3 +123,26 @@ export const searchListings = searchParams =>
|
|||
throw e;
|
||||
});
|
||||
};
|
||||
|
||||
export const searchMapListings = searchParams =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(searchMapListingsRequest(searchParams));
|
||||
|
||||
const { origin, include = [], page = 1, perPage } = searchParams;
|
||||
|
||||
// TODO: API can't handle camelCase request parameter yet.
|
||||
const searchOrQuery = origin
|
||||
? sdk.listings.search({ ...searchParams, page, per_page: perPage })
|
||||
: sdk.listings.query({ include, page, per_page: perPage });
|
||||
|
||||
return searchOrQuery
|
||||
.then(response => {
|
||||
dispatch(addMarketplaceEntities(response));
|
||||
dispatch(searchMapListingsSuccess(response));
|
||||
return response;
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(searchMapListingsError(e));
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,145 +1,194 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
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 { unionWith } from 'lodash';
|
||||
import config from '../../config';
|
||||
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 { PageLayout, SearchResultsPanel, Topbar } from '../../components';
|
||||
import { searchListings } from './SearchPage.duck';
|
||||
import { SearchMap, ModalInMobile, PageLayout, SearchResultsPanel, Topbar } from '../../components';
|
||||
import { searchListings, searchMapListings } from './SearchPage.duck';
|
||||
import css from './SearchPage.css';
|
||||
|
||||
// TODO Pagination page size might need to be dynamic on responsive page layouts
|
||||
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 pickSearchParamsOnly = params => {
|
||||
const { address, origin, bounds } = params || {};
|
||||
return { address, origin, bounds };
|
||||
};
|
||||
|
||||
export const SearchPageComponent = props => {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
history,
|
||||
isAuthenticated,
|
||||
listings,
|
||||
location,
|
||||
logoutError,
|
||||
notificationCount,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
pagination,
|
||||
scrollingDisabled,
|
||||
searchInProgress,
|
||||
searchListingsError,
|
||||
searchParams,
|
||||
tab,
|
||||
} = props;
|
||||
export class SearchPageComponent extends Component {
|
||||
componentDidMount() {
|
||||
const { location, 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 };
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { page, ...searchInURL } = parse(location.search, {
|
||||
latlng: ['origin'],
|
||||
latlngBounds: ['bounds'],
|
||||
});
|
||||
// Search more listings
|
||||
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`);
|
||||
});
|
||||
}
|
||||
|
||||
// Page transition might initially use values from previous search
|
||||
const searchParamsInURL = stringify(pickSearchParamsOnly(searchInURL));
|
||||
const searchParamsInProps = stringify(pickSearchParamsOnly(searchParams));
|
||||
const searchParamsMatch = searchParamsInURL === searchParamsInProps;
|
||||
render() {
|
||||
const {
|
||||
authInfoError,
|
||||
authInProgress,
|
||||
currentUser,
|
||||
currentUserHasListings,
|
||||
history,
|
||||
isAuthenticated,
|
||||
listings,
|
||||
location,
|
||||
logoutError,
|
||||
mapListings,
|
||||
notificationCount,
|
||||
onLogout,
|
||||
onManageDisableScrolling,
|
||||
pagination,
|
||||
scrollingDisabled,
|
||||
searchInProgress,
|
||||
searchListingsError,
|
||||
searchParams,
|
||||
tab,
|
||||
} = this.props;
|
||||
|
||||
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
|
||||
const totalItems = searchParamsMatch && hasPaginationInfo ? pagination.totalItems : 0;
|
||||
const listingsAreLoaded = !searchInProgress && searchParamsMatch && hasPaginationInfo;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { page, ...searchInURL } = parse(location.search, {
|
||||
latlng: ['origin'],
|
||||
latlngBounds: ['bounds'],
|
||||
});
|
||||
|
||||
const searchError = (
|
||||
<h2 className={css.error}>
|
||||
<FormattedMessage id="SearchPage.searchError" />
|
||||
</h2>
|
||||
);
|
||||
// Page transition might initially use values from previous search
|
||||
const searchParamsInURL = stringify(pickSearchParamsOnly(searchInURL));
|
||||
const searchParamsInProps = stringify(pickSearchParamsOnly(searchParams));
|
||||
const searchParamsMatch = searchParamsInURL === searchParamsInProps;
|
||||
|
||||
const resultsFoundNoAddress = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.foundResults" values={{ count: totalItems }} />
|
||||
</h2>
|
||||
);
|
||||
const address = searchInURL && searchInURL.address
|
||||
? <span className={css.searchString}>{searchInURL.address.split(', ')[0]}</span>
|
||||
: null;
|
||||
const resultsFoundWithAddress = (
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="SearchPage.foundResultsWithAddress"
|
||||
values={{ count: totalItems, address }}
|
||||
/>
|
||||
</h2>
|
||||
);
|
||||
const resultsFound = address ? resultsFoundWithAddress : resultsFoundNoAddress;
|
||||
const { address, bounds, origin } = searchInURL || {};
|
||||
|
||||
const noResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.noResults" />
|
||||
</h2>
|
||||
);
|
||||
const hasPaginationInfo = !!pagination && pagination.totalItems != null;
|
||||
const totalItems = searchParamsMatch && hasPaginationInfo ? pagination.totalItems : 0;
|
||||
const listingsAreLoaded = !searchInProgress && searchParamsMatch && hasPaginationInfo;
|
||||
|
||||
const loadingResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.loadingResults" />
|
||||
</h2>
|
||||
);
|
||||
const searchError = (
|
||||
<h2 className={css.error}>
|
||||
<FormattedMessage id="SearchPage.searchError" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const searchParamsForPagination = parse(location.search);
|
||||
const resultsFoundNoAddress = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.foundResults" values={{ count: totalItems }} />
|
||||
</h2>
|
||||
);
|
||||
const addressNode = address
|
||||
? <span className={css.searchString}>{searchInURL.address.split(', ')[0]}</span>
|
||||
: null;
|
||||
const resultsFoundWithAddress = (
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="SearchPage.foundResultsWithAddress"
|
||||
values={{ count: totalItems, address: addressNode }}
|
||||
/>
|
||||
</h2>
|
||||
);
|
||||
const resultsFound = address ? resultsFoundWithAddress : resultsFoundNoAddress;
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
title={`Search page: ${tab}`}
|
||||
scrollingDisabled={scrollingDisabled}
|
||||
>
|
||||
<Topbar
|
||||
isAuthenticated={isAuthenticated}
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
notificationCount={notificationCount}
|
||||
history={history}
|
||||
location={location}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<div className={css.searchResultSummary}>
|
||||
{searchListingsError ? searchError : null}
|
||||
{listingsAreLoaded && totalItems > 0 ? resultsFound : null}
|
||||
{listingsAreLoaded && totalItems === 0 ? noResults : null}
|
||||
{searchInProgress ? loadingResults : null}
|
||||
</div>
|
||||
<div className={css.container}>
|
||||
<div className={css.listings}>
|
||||
<SearchResultsPanel
|
||||
className={css.searchListingsPanel}
|
||||
currencyConfig={config.currencyConfig}
|
||||
listings={listingsAreLoaded ? listings : []}
|
||||
pagination={listingsAreLoaded ? pagination : null}
|
||||
search={searchParamsForPagination}
|
||||
/>
|
||||
const noResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.noResults" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const loadingResults = (
|
||||
<h2>
|
||||
<FormattedMessage id="SearchPage.loadingResults" />
|
||||
</h2>
|
||||
);
|
||||
|
||||
const searchParamsForPagination = parse(location.search);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
authInfoError={authInfoError}
|
||||
logoutError={logoutError}
|
||||
title={`Search page: ${tab}`}
|
||||
scrollingDisabled={scrollingDisabled}
|
||||
>
|
||||
<Topbar
|
||||
className={css.topbar}
|
||||
isAuthenticated={isAuthenticated}
|
||||
authInProgress={authInProgress}
|
||||
currentUser={currentUser}
|
||||
currentUserHasListings={currentUserHasListings}
|
||||
notificationCount={notificationCount}
|
||||
history={history}
|
||||
location={location}
|
||||
onLogout={onLogout}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
/>
|
||||
<div className={css.container}>
|
||||
<div className={css.searchResultContainer}>
|
||||
<div className={css.searchResultSummary}>
|
||||
{searchListingsError ? searchError : null}
|
||||
{listingsAreLoaded && totalItems > 0 ? resultsFound : null}
|
||||
{listingsAreLoaded && totalItems === 0 ? noResults : null}
|
||||
{searchInProgress ? loadingResults : null}
|
||||
</div>
|
||||
<div className={css.listings}>
|
||||
<SearchResultsPanel
|
||||
className={css.searchListingsPanel}
|
||||
currencyConfig={config.currencyConfig}
|
||||
listings={listingsAreLoaded ? listings : []}
|
||||
pagination={listingsAreLoaded ? pagination : null}
|
||||
search={searchParamsForPagination}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModalInMobile
|
||||
className={css.mapPanel}
|
||||
id="SearchPage.map"
|
||||
isModalOpenOnMobile={false}
|
||||
onManageDisableScrolling={onManageDisableScrolling}
|
||||
>
|
||||
<div className={css.map}>
|
||||
<SearchMap bounds={bounds} center={origin} listings={mapListings || []} />
|
||||
</div>
|
||||
</ModalInMobile>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchPageComponent.defaultProps = {
|
||||
authInfoError: null,
|
||||
currentUser: null,
|
||||
listings: [],
|
||||
logoutError: null,
|
||||
mapListings: [],
|
||||
notificationCount: 0,
|
||||
pagination: null,
|
||||
searchListingsError: null,
|
||||
|
|
@ -156,10 +205,12 @@ SearchPageComponent.propTypes = {
|
|||
currentUserHasListings: bool.isRequired,
|
||||
isAuthenticated: bool.isRequired,
|
||||
listings: array,
|
||||
mapListings: array,
|
||||
logoutError: instanceOf(Error),
|
||||
notificationCount: number,
|
||||
onLogout: func.isRequired,
|
||||
onManageDisableScrolling: func.isRequired,
|
||||
onSearchMapListings: func.isRequired,
|
||||
pagination: propTypes.pagination,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
searchInProgress: bool.isRequired,
|
||||
|
|
@ -183,6 +234,7 @@ const mapStateToProps = state => {
|
|||
searchInProgress,
|
||||
searchListingsError,
|
||||
searchParams,
|
||||
searchMapListingIds,
|
||||
} = state.SearchPage;
|
||||
const { authInfoError, isAuthenticated, logoutError } = state.Auth;
|
||||
const {
|
||||
|
|
@ -190,10 +242,17 @@ const mapStateToProps = state => {
|
|||
currentUserHasListings,
|
||||
currentUserNotificationCount: notificationCount,
|
||||
} = state.user;
|
||||
const pageListings = getListingsById(state, currentPageResultIds);
|
||||
const mapListings = getListingsById(
|
||||
state,
|
||||
unionWith(currentPageResultIds, searchMapListingIds, (id1, id2) => id1.uuid === id2.uuid)
|
||||
);
|
||||
|
||||
return {
|
||||
authInfoError,
|
||||
logoutError,
|
||||
listings: getListingsById(state, currentPageResultIds),
|
||||
listings: pageListings,
|
||||
mapListings,
|
||||
pagination,
|
||||
searchInProgress,
|
||||
searchListingsError,
|
||||
|
|
@ -211,6 +270,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
onLogout: historyPush => dispatch(logout(historyPush)),
|
||||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
onSearchMapListings: searchParams => dispatch(searchMapListings(searchParams)),
|
||||
});
|
||||
|
||||
const SearchPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ describe('SearchPageComponent', () => {
|
|||
isAuthenticated: false,
|
||||
onLogout: noop,
|
||||
onManageDisableScrolling: noop,
|
||||
onSearchMapListings: noop,
|
||||
};
|
||||
const tree = renderShallow(<SearchPageComponent {...props} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
|
|
@ -22,44 +22,68 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
notificationCount={0}
|
||||
onLogout={[Function]}
|
||||
onManageDisableScrolling={[Function]} />
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="SearchPage.foundResults"
|
||||
values={
|
||||
Object {
|
||||
"count": 22,
|
||||
}
|
||||
} />
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<SearchResultsPanel
|
||||
className={null}
|
||||
currencyConfig={
|
||||
Object {
|
||||
"currency": "USD",
|
||||
"currencyDisplay": "symbol",
|
||||
"maximumFractionDigits": 2,
|
||||
"minimumFractionDigits": 2,
|
||||
"style": "currency",
|
||||
"subUnitDivisor": 100,
|
||||
"useGrouping": true,
|
||||
<div>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="SearchPage.foundResults"
|
||||
values={
|
||||
Object {
|
||||
"count": 22,
|
||||
}
|
||||
} />
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<SearchResultsPanel
|
||||
className={null}
|
||||
currencyConfig={
|
||||
Object {
|
||||
"currency": "USD",
|
||||
"currencyDisplay": "symbol",
|
||||
"maximumFractionDigits": 2,
|
||||
"minimumFractionDigits": 2,
|
||||
"style": "currency",
|
||||
"subUnitDivisor": 100,
|
||||
"useGrouping": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
listings={Array []}
|
||||
pagination={
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 12,
|
||||
"totalItems": 22,
|
||||
"totalPages": 2,
|
||||
listings={Array []}
|
||||
pagination={
|
||||
Object {
|
||||
"page": 1,
|
||||
"perPage": 12,
|
||||
"totalItems": 22,
|
||||
"totalPages": 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
rootClassName={null}
|
||||
search={Object {}} />
|
||||
rootClassName={null}
|
||||
search={Object {}} />
|
||||
</div>
|
||||
</div>
|
||||
<ModalInMobile
|
||||
className=""
|
||||
id="SearchPage.map"
|
||||
isModalOpenOnMobile={false}
|
||||
onClose={null}
|
||||
onManageDisableScrolling={[Function]}
|
||||
showAsModalMaxWidth={0}>
|
||||
<div>
|
||||
<SearchMap
|
||||
bounds={null}
|
||||
center={
|
||||
LatLng {
|
||||
"lat": 0,
|
||||
"lng": 0,
|
||||
}
|
||||
}
|
||||
className=""
|
||||
listings={Array []}
|
||||
rootClassName={null}
|
||||
zoom={11} />
|
||||
</div>
|
||||
</ModalInMobile>
|
||||
</div>
|
||||
</withRouter(PageLayout)>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ const TopbarSearchFormComponent = props => {
|
|||
|
||||
const { func, string, bool } = PropTypes;
|
||||
|
||||
TopbarSearchFormComponent.defaultProps = { rootClassName: null, className: null, desktopInputRoot: null, isMobile: false };
|
||||
TopbarSearchFormComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
desktopInputRoot: null,
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
TopbarSearchFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@
|
|||
"LoginForm.passwordLabel": "Password",
|
||||
"LoginForm.passwordPlaceholder": "Enter your password…",
|
||||
"LoginForm.passwordRequired": "This field is required",
|
||||
"MapPriceMarker.unsupportedPrice": "({currency})",
|
||||
"Modal.close": "CLOSE",
|
||||
"Modal.closeModal": "Close modal",
|
||||
"OrderDetailsPanel.bookingBreakdownTitle": "Booking breakdown",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue