SearchMapListingCard opens when price 'marker' is clicked

This commit is contained in:
Vesa Luusua 2017-08-15 11:54:49 +03:00
parent 7a6ac4506f
commit 90adf90c13
4 changed files with 265 additions and 2 deletions

View file

@ -39,17 +39,21 @@ import NoImageIcon from './NoImageIcon';
import css from './ResponsiveImage.css';
const ResponsiveImage = props => {
const { className, rootClassName, alt, image, nameSet, sizes } = props;
const { className, rootClassName, alt, noImageMessage, image, nameSet, sizes } = props;
const classes = classNames(rootClassName || css.root, className);
if (image == null || nameSet.length === 0) {
const noImageClasses = classNames(rootClassName || css.root, css.noImageContainer, className);
// NoImageMessage is needed for listing images on top the map (those component lose context)
// https://github.com/tomchentw/react-google-maps/issues/578
const noImageMessageText = noImageMessage || <FormattedMessage id="ResponsiveImage.noImage" />;
/* eslint-disable jsx-a11y/img-redundant-alt */
return (
<div className={noImageClasses}>
<div className={css.noImageWrapper}>
<NoImageIcon className={css.noImageIcon} />
<div className={css.noImageText}><FormattedMessage id="ResponsiveImage.noImage" /></div>
<div className={css.noImageText}>{noImageMessageText}</div>
</div>
</div>
);
@ -79,6 +83,7 @@ ResponsiveImage.defaultProps = {
image: null,
nameSet: [],
sizes: null,
noImageMessage: null,
};
ResponsiveImage.propTypes = {
@ -93,6 +98,7 @@ ResponsiveImage.propTypes = {
})
),
sizes: string,
noImageMessage: string,
};
export default ResponsiveImage;

View file

@ -0,0 +1,132 @@
@import '../../marketplace.css';
.root {
display: block;
position: relative;
height: auto;
border: 0;
padding: 0;
}
.anchor {
&:hover {
text-decoration: none;
}
}
.card {
/**
* Since caret is absolutely positioned,
* label must have relative to be included to the same rendering layer
*/
position: relative;
width: 250px;
min-height: 207px;
/* Font */
@apply --marketplaceH5FontStyles;
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 */
margin-top: 0;
margin-bottom: 0;
transition: var(--transitionStyleButton);
&:hover {
cursor: pointer;
box-shadow: var(--boxShadowPopup);
}
/* Overwrite dimensions from font styles */
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.threeToTwoWrapper {
/* Layout */
display: block;
width: 100%;
position: relative;
}
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectWrapper {
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
background: var(--matterColorNegative); /* Loading BG color */
}
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
border-radius: 2px;
}
.info {
display: flex;
padding: 8px 16px;
}
.price {
flex-shrink: 0;
font-weight: var(--fontWeightSemiBold);
color: var(--marketplaceColor);
margin-right: 10px;
}
.name {
flex-grow: 1;
}
.caretShadow {
/* Caret / arrow dimensions and position */
width: 6px;
height: 6px;
position: absolute;
bottom: -3px;
left: 50%;
margin-left: -3px;
transform: rotate(45deg);
/* Caret should have same box-shadow as label */
box-shadow: var(--boxShadowPopupLight);
}
.caret {
/* Caret / arrow dimensions and position */
width: 6px;
height: 6px;
position: absolute;
bottom: -3px;
left: 50%;
margin-left: -3px;
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;
}

View file

@ -0,0 +1,123 @@
import React, { Component, PropTypes } from 'react';
import { OverlayView } from 'react-google-maps';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { formatMoney } from '../../util/currency';
import config from '../../config';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { createResourceLocatorString } from '../../util/routes';
import { ResponsiveImage } from '../../components';
import css from './SearchMapListingCard.css';
// Center label so that caret is pointing to correct pixel.
// (vertical positioning: height + arrow) */
const getPixelPositionOffset = (width, height) => {
return { x: -1 * (width / 2), y: -1 * (height + 3) };
};
const createURL = (flattenedRoutes, history, listing) => {
const id = listing.id.uuid;
const slug = encodeURIComponent(listing.attributes.title.split(' ').join('-'));
const pathParams = { id, slug };
return createResourceLocatorString('ListingPage', flattenedRoutes, pathParams, {});
};
class SearchMapListingCard extends Component {
constructor(props) {
super(props);
this.clickHandler = this.clickHandler.bind(this);
}
clickHandler(e) {
e.preventDefault();
// To avoid full page refresh we need to use internal router
const { flattenedRoutes, history, listing } = this.props;
history.push(createURL(flattenedRoutes, history, listing));
}
render() {
const { className, rootClassName, intl, flattenedRoutes, history, listing } = this.props;
const { title, price } = listing.attributes;
const formattedPrice = price && price.currency === config.currencyConfig.currency
? formatMoney(intl, config.currencyConfig, price)
: price.currency;
const firstImage = listing.images && listing.images.length > 0 ? listing.images[0] : null;
const geolocation = listing.attributes.geolocation;
const urlToListing = createURL(flattenedRoutes, history, listing);
// Explicit type change to object literal for Google OverlayViews (geolocation is SDK type)
const latLngLiteral = { lat: geolocation.lat, lng: geolocation.lng };
const classes = classNames(rootClassName || css.root, className);
return (
<OverlayView
position={latLngLiteral}
mapPaneName={OverlayView.FLOAT_PANE}
getPixelPositionOffset={getPixelPositionOffset}
styles={{ zIndex: 1 }}
>
<div className={classes}>
<a alt={title} className={css.anchor} href={urlToListing} onClick={this.clickHandler}>
<div className={css.caretShadow} />
<div className={css.card}>
<div className={css.threeToTwoWrapper}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={title}
noImageMessage={intl.formatMessage({ id: 'SearchMapListingCard.noImage' })}
image={firstImage}
nameSet={[
{ name: 'landscape-crop', size: '1x' },
{ name: 'landscape-crop2x', size: '2x' },
]}
/>
</div>
</div>
<div className={css.info}>
<div className={css.price}>
{formattedPrice}
</div>
<div className={css.name}>
{title}
</div>
</div>
</div>
<div className={css.caret} />
</a>
</div>
</OverlayView>
);
}
}
SearchMapListingCard.defaultProps = {
className: null,
rootClassName: null,
};
const { arrayOf, func, shape, string } = PropTypes;
SearchMapListingCard.propTypes = {
className: string,
rootClassName: string,
listing: propTypes.listing.isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
export default compose(withRouter, withFlattenedRoutes, injectIntl)(SearchMapListingCard);

View file

@ -42,6 +42,7 @@ import RoutesProvider from './RoutesProvider/RoutesProvider';
import SaleDetailsPanel from './SaleDetailsPanel/SaleDetailsPanel';
import SearchIcon from './SearchIcon/SearchIcon';
import SearchMap from './SearchMap/SearchMap';
import SearchMapListingCard from './SearchMapListingCard/SearchMapListingCard';
import SearchMapPriceLabel from './SearchMapPriceLabel/SearchMapPriceLabel';
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
import SelectField from './SelectField/SelectField';
@ -103,6 +104,7 @@ export {
SaleDetailsPanel,
SearchIcon,
SearchMap,
SearchMapListingCard,
SearchMapPriceLabel,
SearchResultsPanel,
SecondaryButton,