mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Merge pull request #743 from sharetribe/map-search-hover
Highlight hovered listing on map
This commit is contained in:
commit
bba4d8f96c
11 changed files with 144 additions and 31 deletions
|
|
@ -50,7 +50,7 @@ const formatTitle = (title, maxLength) => {
|
|||
};
|
||||
|
||||
export const ListingCardComponent = props => {
|
||||
const { className, rootClassName, intl, listing } = props;
|
||||
const { className, rootClassName, intl, listing, setActiveListing } = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const currentListing = ensureListing(listing);
|
||||
const id = currentListing.id.uuid;
|
||||
|
|
@ -65,7 +65,11 @@ export const ListingCardComponent = props => {
|
|||
|
||||
return (
|
||||
<NamedLink className={classes} name="ListingPage" params={{ id, slug }}>
|
||||
<div className={css.threeToTwoWrapper}>
|
||||
<div
|
||||
className={css.threeToTwoWrapper}
|
||||
onMouseEnter={() => setActiveListing(currentListing.id)}
|
||||
onMouseLeave={() => setActiveListing(null)}
|
||||
>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
rootClassName={css.rootForImage}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ exports[`ListingCard matches snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div>
|
||||
<ResponsiveImage
|
||||
alt="listing1 title"
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ const withCoordinatesObfuscated = listings => {
|
|||
*/
|
||||
const MapWithGoogleMap = withGoogleMap(props => {
|
||||
const {
|
||||
activeListingId,
|
||||
center,
|
||||
infoCardOpen,
|
||||
isOpenOnModal,
|
||||
|
|
@ -103,6 +104,10 @@ const MapWithGoogleMap = withGoogleMap(props => {
|
|||
|
||||
const listingArraysInLocations = reducedToArray(groupedByCoordinates(listings));
|
||||
const priceLabels = listingArraysInLocations.reverse().map(listingArr => {
|
||||
const isActive = activeListingId
|
||||
? !!listingArr.find(l => activeListingId.uuid === l.id.uuid)
|
||||
: false;
|
||||
|
||||
// If location contains only one listing, print price label
|
||||
if (listingArr.length === 1) {
|
||||
const listing = listingArr[0];
|
||||
|
|
@ -114,6 +119,7 @@ const MapWithGoogleMap = withGoogleMap(props => {
|
|||
}
|
||||
return (
|
||||
<SearchMapPriceLabel
|
||||
isActive={isActive}
|
||||
key={listing.id.uuid}
|
||||
className={LABEL_HANDLE}
|
||||
listing={listing}
|
||||
|
|
@ -123,6 +129,7 @@ const MapWithGoogleMap = withGoogleMap(props => {
|
|||
}
|
||||
return (
|
||||
<SearchMapGroupLabel
|
||||
isActive={isActive}
|
||||
key={listingArr[0].id.uuid}
|
||||
className={LABEL_HANDLE}
|
||||
listings={listingArr}
|
||||
|
|
@ -237,6 +244,7 @@ export class SearchMapComponent extends Component {
|
|||
onIdle,
|
||||
zoom,
|
||||
coordinatesConfig,
|
||||
activeListingId,
|
||||
} = this.props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const mapClasses = mapRootClassName || css.mapRoot;
|
||||
|
|
@ -255,6 +263,7 @@ export class SearchMapComponent extends Component {
|
|||
center={center}
|
||||
isOpenOnModal={isOpenOnModal}
|
||||
listings={listings}
|
||||
activeListingId={activeListingId}
|
||||
infoCardOpen={this.state.infoCardOpen}
|
||||
onListingClicked={this.onListingClicked}
|
||||
onMapLoad={this.onMapLoadHandler}
|
||||
|
|
@ -276,6 +285,7 @@ export class SearchMapComponent extends Component {
|
|||
SearchMapComponent.defaultProps = {
|
||||
bounds: null,
|
||||
center: new sdkTypes.LatLng(0, 0),
|
||||
activeListingId: null,
|
||||
className: null,
|
||||
isOpenOnModal: false,
|
||||
listings: [],
|
||||
|
|
@ -290,6 +300,7 @@ SearchMapComponent.defaultProps = {
|
|||
SearchMapComponent.propTypes = {
|
||||
bounds: propTypes.latlngBounds,
|
||||
center: propTypes.latlng,
|
||||
activeListingId: propTypes.uuid,
|
||||
className: string,
|
||||
isOpenOnModal: bool,
|
||||
listings: arrayOf(propTypes.listing),
|
||||
|
|
|
|||
|
|
@ -7,29 +7,48 @@
|
|||
height: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
& .details,
|
||||
& .caret {
|
||||
cursor: pointer;
|
||||
|
||||
/* Same as active */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
color: var(--matterColorLight);
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
/**
|
||||
* Since caret is absolutely positioned,
|
||||
* label must have relative to be included to the same rendering layer
|
||||
*/
|
||||
position: relative;
|
||||
|
||||
/* Font */
|
||||
@apply --marketplaceH4FontStyles;
|
||||
font-weight: var(--fontWeightSemiBold);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
/* Coloring */
|
||||
border: 0;
|
||||
padding: 6px 14px;
|
||||
|
||||
/* Coloring */
|
||||
background-color: var(--matterColorLight);
|
||||
color: var(--matterColor);
|
||||
|
||||
/* Borders */
|
||||
border-radius: 17px;
|
||||
background-color: var(--marketplaceColor);
|
||||
color: var(--matterColorLight);
|
||||
|
||||
border-style: solid;
|
||||
border-color: var(--matterColorNegative);
|
||||
border-width: 1px;
|
||||
box-shadow: var(--boxShadowPopupLight);
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
/* Overwrite dimensions from font styles */
|
||||
@media (--viewportMedium) {
|
||||
|
|
@ -38,6 +57,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.detailsActive {
|
||||
/* Same as hover */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
color: var(--matterColorLight);
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
|
||||
.caretShadow {
|
||||
/* Caret / arrow dimensions and position */
|
||||
width: 6px;
|
||||
|
|
@ -63,5 +90,13 @@
|
|||
transform: rotate(45deg);
|
||||
|
||||
/* Caret should have same bg-color as label */
|
||||
background-color: var(--marketplaceColor);
|
||||
background-color: var(--matterColorLight);
|
||||
|
||||
transition: var(--transitionStyleButton);
|
||||
}
|
||||
|
||||
.caretActive {
|
||||
/* Same as hover */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,17 +15,22 @@ const getPixelPositionOffset = (width, height) => {
|
|||
|
||||
class SearchMapGroupLabel extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.listings.length > this.props.listings.length;
|
||||
const hasSameAmountOfListings = nextProps.listings.length === this.props.listings.length;
|
||||
const hasSameActiveStatus = this.props.isActive === nextProps.isActive;
|
||||
|
||||
return !(hasSameAmountOfListings && hasSameActiveStatus);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, rootClassName, listings, onListingClicked } = this.props;
|
||||
const { className, rootClassName, listings, onListingClicked, isActive } = this.props;
|
||||
const firstListing = ensureListing(listings[0]);
|
||||
const geolocation = firstListing.attributes.geolocation;
|
||||
|
||||
// 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);
|
||||
const countLabelClasses = classNames(css.details, { [css.detailsActive]: isActive });
|
||||
const caretClasses = classNames(css.caret, { [css.caretActive]: isActive });
|
||||
|
||||
return (
|
||||
<OverlayView
|
||||
|
|
@ -35,8 +40,8 @@ class SearchMapGroupLabel extends Component {
|
|||
>
|
||||
<button className={classes} onClick={() => onListingClicked(listings)}>
|
||||
<div className={css.caretShadow} />
|
||||
<div className={css.details}>{listings.length}</div>
|
||||
<div className={css.caret} />
|
||||
<div className={countLabelClasses}>{listings.length}</div>
|
||||
<div className={caretClasses} />
|
||||
</button>
|
||||
</OverlayView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@
|
|||
height: auto;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
& .priceLabel,
|
||||
& .caret {
|
||||
cursor: pointer;
|
||||
|
||||
/* Same as active */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
color: var(--matterColorLight);
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.priceLabel {
|
||||
|
|
@ -36,12 +49,6 @@
|
|||
margin-bottom: 0;
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
&:hover {
|
||||
color: var(--marketplaceColor);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
|
||||
/* Overwrite dimensions from font styles */
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 0;
|
||||
|
|
@ -49,6 +56,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.priceLabelActive {
|
||||
/* Same as hover */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
color: var(--matterColorLight);
|
||||
box-shadow: var(--boxShadowPopup);
|
||||
}
|
||||
|
||||
.caretShadow {
|
||||
/* Caret / arrow dimensions and position */
|
||||
width: 6px;
|
||||
|
|
@ -73,6 +88,8 @@
|
|||
margin-left: -3px;
|
||||
transform: rotate(45deg);
|
||||
|
||||
transition: var(--transitionStyleButton);
|
||||
|
||||
/* Caret should have same bg-color and border as label */
|
||||
background-color: var(--matterColorLight);
|
||||
border-right-style: solid;
|
||||
|
|
@ -82,3 +99,9 @@
|
|||
border-bottom-color: var(--matterColorNegative);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.caretActive {
|
||||
/* Same as hover */
|
||||
background-color: var(--marketplaceColor);
|
||||
border-color: var(--marketplaceColor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ class SearchMapPriceLabel extends Component {
|
|||
const nextListing = ensureListing(nextProps.listing);
|
||||
const isSameListing = currentListing.id.uuid === nextListing.id.uuid;
|
||||
const hasSamePrice = currentListing.attributes.price === nextListing.attributes.price;
|
||||
return !(isSameListing && hasSamePrice);
|
||||
const hasSameActiveStatus = this.props.isActive === nextProps.isActive;
|
||||
|
||||
return !(isSameListing && hasSamePrice && hasSameActiveStatus);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, rootClassName, intl, listing, onListingClicked } = this.props;
|
||||
const { className, rootClassName, intl, listing, onListingClicked, isActive } = this.props;
|
||||
const currentListing = ensureListing(listing);
|
||||
const { geolocation, price } = currentListing.attributes;
|
||||
|
||||
|
|
@ -37,6 +39,8 @@ class SearchMapPriceLabel extends Component {
|
|||
// 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);
|
||||
const priceLabelClasses = classNames(css.priceLabel, { [css.priceLabelActive]: isActive });
|
||||
const caretClasses = classNames(css.caret, { [css.caretActive]: isActive });
|
||||
|
||||
return (
|
||||
<OverlayView
|
||||
|
|
@ -46,8 +50,8 @@ class SearchMapPriceLabel extends Component {
|
|||
>
|
||||
<button className={classes} onClick={() => onListingClicked(currentListing)}>
|
||||
<div className={css.caretShadow} />
|
||||
<div className={css.priceLabel}>{formattedPrice}</div>
|
||||
<div className={css.caret} />
|
||||
<div className={priceLabelClasses}>{formattedPrice}</div>
|
||||
<div className={caretClasses} />
|
||||
</button>
|
||||
</OverlayView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ListingCard, PaginationLinks } from '../../components';
|
|||
import css from './SearchResultsPanel.css';
|
||||
|
||||
const SearchResultsPanel = props => {
|
||||
const { className, rootClassName, listings, pagination, search } = props;
|
||||
const { className, rootClassName, listings, pagination, search, setActiveListing } = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
||||
const paginationLinks =
|
||||
|
|
@ -22,7 +22,14 @@ const SearchResultsPanel = props => {
|
|||
return (
|
||||
<div className={classes}>
|
||||
<div className={css.listingCards}>
|
||||
{listings.map(l => <ListingCard className={css.listingCard} key={l.id.uuid} listing={l} />)}
|
||||
{listings.map(l => (
|
||||
<ListingCard
|
||||
className={css.listingCard}
|
||||
key={l.id.uuid}
|
||||
listing={l}
|
||||
setActiveListing={setActiveListing}
|
||||
/>
|
||||
))}
|
||||
{props.children}
|
||||
</div>
|
||||
{paginationLinks}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export const SEARCH_MAP_LISTINGS_REQUEST = 'app/SearchPage/SEARCH_MAP_LISTINGS_R
|
|||
export const SEARCH_MAP_LISTINGS_SUCCESS = 'app/SearchPage/SEARCH_MAP_LISTINGS_SUCCESS';
|
||||
export const SEARCH_MAP_LISTINGS_ERROR = 'app/SearchPage/SEARCH_MAP_LISTINGS_ERROR';
|
||||
|
||||
export const SEARCH_MAP_SET_ACTIVE_LISTING = 'app/SearchPage/SEARCH_MAP_SET_ACTIVE_LISTING';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -70,6 +72,11 @@ const listingPageReducer = (state = initialState, action = {}) => {
|
|||
console.error(payload);
|
||||
return { ...state, searchMapListingsError: payload };
|
||||
|
||||
case SEARCH_MAP_SET_ACTIVE_LISTING:
|
||||
return {
|
||||
...state,
|
||||
activeListingId: payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -130,6 +137,11 @@ export const searchListings = searchParams => (dispatch, getState, sdk) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const setActiveListing = listingId => ({
|
||||
type: SEARCH_MAP_SET_ACTIVE_LISTING,
|
||||
payload: listingId,
|
||||
});
|
||||
|
||||
export const searchMapListings = searchParams => (dispatch, getState, sdk) => {
|
||||
dispatch(searchMapListingsRequest(searchParams));
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
SearchFiltersPanel,
|
||||
} from '../../components';
|
||||
import { TopbarContainer } from '../../containers';
|
||||
import { searchListings, searchMapListings } from './SearchPage.duck';
|
||||
import { searchListings, searchMapListings, setActiveListing } from './SearchPage.duck';
|
||||
|
||||
import css from './SearchPage.css';
|
||||
|
||||
|
|
@ -260,6 +260,8 @@ export class SearchPageComponent extends Component {
|
|||
searchParams,
|
||||
categories,
|
||||
amenities,
|
||||
activeListingId,
|
||||
onActivateListing,
|
||||
} = this.props;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { mapSearch, page, ...searchInURL } = parse(location.search, {
|
||||
|
|
@ -292,6 +294,7 @@ export class SearchPageComponent extends Component {
|
|||
|
||||
const searchMap = (
|
||||
<SearchMap
|
||||
activeListingId={activeListingId}
|
||||
bounds={bounds}
|
||||
center={origin}
|
||||
listings={mapListings || []}
|
||||
|
|
@ -369,6 +372,7 @@ export class SearchPageComponent extends Component {
|
|||
listings={listings}
|
||||
pagination={listingsAreLoaded ? pagination : null}
|
||||
search={searchParamsForPagination}
|
||||
setActiveListing={onActivateListing}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -475,6 +479,7 @@ SearchPageComponent.defaultProps = {
|
|||
tab: 'listings',
|
||||
categories: config.custom.categories,
|
||||
amenities: config.custom.amenities,
|
||||
activeListingId: null,
|
||||
};
|
||||
|
||||
const { array, bool, func, oneOf, object, shape, string } = PropTypes;
|
||||
|
|
@ -513,6 +518,7 @@ const mapStateToProps = state => {
|
|||
searchListingsError,
|
||||
searchParams,
|
||||
searchMapListingIds,
|
||||
activeListingId,
|
||||
} = state.SearchPage;
|
||||
const pageListings = getListingsById(state, currentPageResultIds);
|
||||
const mapListings = getListingsById(
|
||||
|
|
@ -528,6 +534,7 @@ const mapStateToProps = state => {
|
|||
searchInProgress,
|
||||
searchListingsError,
|
||||
searchParams,
|
||||
activeListingId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -535,6 +542,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
onManageDisableScrolling: (componentId, disableScrolling) =>
|
||||
dispatch(manageDisableScrolling(componentId, disableScrolling)),
|
||||
onSearchMapListings: searchParams => dispatch(searchMapListings(searchParams)),
|
||||
onActivateListing: listingId => dispatch(setActiveListing(listingId)),
|
||||
});
|
||||
|
||||
// Note: it is important that the withRouter HOC is **outside** the
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
|
|||
>
|
||||
<div>
|
||||
<SearchMapComponent
|
||||
activeListingId={null}
|
||||
bounds={null}
|
||||
center={
|
||||
LatLng {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue