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 (
-
+
{noImageMessageText}
); @@ -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 ( + +
+ +
+
+
+
+ +
+
+
+
+ {formattedPrice} +
+
+ {title} +
+
+
+
+ +
+ + ); + } +} + +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,