diff --git a/src/components/ResponsiveImage/ResponsiveImage.js b/src/components/ResponsiveImage/ResponsiveImage.js
index 17b514d4..5039e25d 100644
--- a/src/components/ResponsiveImage/ResponsiveImage.js
+++ b/src/components/ResponsiveImage/ResponsiveImage.js
@@ -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 || ;
/* eslint-disable jsx-a11y/img-redundant-alt */
return (
);
@@ -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;
diff --git a/src/components/SearchMapListingCard/SearchMapListingCard.css b/src/components/SearchMapListingCard/SearchMapListingCard.css
new file mode 100644
index 00000000..6ea97c83
--- /dev/null
+++ b/src/components/SearchMapListingCard/SearchMapListingCard.css
@@ -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;
+
+}
diff --git a/src/components/SearchMapListingCard/SearchMapListingCard.js b/src/components/SearchMapListingCard/SearchMapListingCard.js
new file mode 100644
index 00000000..2c627558
--- /dev/null
+++ b/src/components/SearchMapListingCard/SearchMapListingCard.js
@@ -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 (
+
+
+
+ );
+ }
+}
+
+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);
diff --git a/src/components/index.js b/src/components/index.js
index 705ec7b3..61168339 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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,