Merged in feat/SW-1549-map-improvements (pull request #1783)

Feat/SW-1549 map improvements

* fix: imported new icon

* refactor: rename component and set map handling to 'greedy'

* fix: show cards for 3s after hover

* refactor: update styles and added HotelPin component

* fix: change from close to back icon

* refactor: update to only use 1 state value for active pin and card

* fix: add click handler when dialog is opened

* fix: performance fixes for the dialog carousel

* fix: added border

* fix: clear timeout on mouseenter

* fix: changed to absolute import

* fix: moved hover state into the store

* fix: renamed store actions


Approved-by: Michael Zetterberg
This commit is contained in:
Tobias Johansson
2025-04-15 13:23:23 +00:00
parent 57cd2f6a7f
commit 9a9789e736
27 changed files with 288 additions and 261 deletions

View File

@@ -0,0 +1,32 @@
.pin {
display: flex;
justify-content: center;
align-items: center;
padding: var(--Spacing-x-half) var(--Spacing-x1) var(--Spacing-x-half)
var(--Spacing-x-half);
border: 2px solid var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Rounded);
background-color: var(--Base-Surface-Primary-light-Normal);
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
gap: var(--Spacing-x1);
width: max-content;
}
.pin.active {
background-color: var(--Primary-Dark-Surface-Normal);
color: var(--Base-Surface-Primary-light-Normal);
}
.pinIcon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--Surface-Brand-Primary-2-Default);
}
.pin.active .pinIcon {
background: var(--Base-Surface-Primary-light-Normal);
}

View File

@@ -0,0 +1,46 @@
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import HotelMarker from "@/components/Maps/Markers/HotelMarker"
import { formatPrice } from "@/utils/numberFormatting"
import styles from "./hotelPin.module.css"
interface HotelPinProps {
isActive: boolean
hotelPrice: number | null
currency: string
}
export default function HotelPin({
isActive,
hotelPrice,
currency,
}: HotelPinProps) {
const intl = useIntl()
const isNotAvailable = !hotelPrice
return (
<div
className={`${styles.pin} ${isActive ? styles.active : ""}`}
data-hotelpin
>
<span className={styles.pinIcon}>
{isNotAvailable ? (
<MaterialIcon
icon="calendar_clock"
size={16}
color={isActive ? "Icon/Interactive/Default" : "Icon/Inverted"}
/>
) : (
<HotelMarker width={16} color={isActive ? "burgundy" : "white"} />
)}
</span>
<Typography variant="Body/Paragraph/mdRegular">
<p>{isNotAvailable ? "—" : formatPrice(intl, hotelPrice, currency)}</p>
</Typography>
</div>
)
}

View File

@@ -1,62 +1,11 @@
.advancedMarker {
height: 32px;
min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
}
.advancedMarker.active {
height: 32px;
min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
}
.dialogContainer {
display: none;
}
.pin {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
padding: var(--Spacing-x-half) var(--Spacing-x1) var(--Spacing-x-half)
var(--Spacing-x-half);
border-radius: var(--Corner-radius-Rounded);
background-color: var(--Base-Surface-Primary-light-Normal);
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
gap: var(--Spacing-x1);
width: max-content;
}
.pin.active {
background-color: var(--Primary-Dark-Surface-Normal);
}
.pinLabel {
display: none;
}
.pin.active .pinLabel {
display: flex;
align-items: center;
gap: var(--Spacing-x2);
text-wrap: nowrap;
}
.pinIcon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--Primary-Dark-Surface-Normal);
}
.pin.active .pinIcon {
background: var(--Base-Surface-Primary-light-Normal);
}
.card {
display: none;
position: absolute;

View File

@@ -2,62 +2,39 @@ import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
} from "@vis.gl/react-google-maps"
import { useCallback, useState } from "react"
import { useIntl } from "react-intl"
import { useCallback } from "react"
import { useHotelsMapStore } from "@/stores/hotels-map"
import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog"
import Body from "@/components/TempDesignSystem/Text/Body"
import { formatPrice } from "@/utils/numberFormatting"
import HotelMarker from "../../Markers/HotelMarker"
import HotelPin from "./HotelPin"
import styles from "./hotelListingMapContent.module.css"
import type { HotelListingMapContentProps } from "@/types/components/hotelReservation/selectHotel/map"
function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
const intl = useIntl()
const [hoveredHotelPin, setHoveredHotelPin] = useState<string | null>(null)
const { activeHotelPin, setActiveHotelPin, setActiveHotelCard } =
const { activeHotel, hoveredHotel, activate, deactivate, engage, disengage } =
useHotelsMapStore()
const toggleActiveHotelPin = useCallback(
(pinName: string | null) => {
if (setActiveHotelPin) {
const newActivePin = activeHotelPin === pinName ? null : pinName
setActiveHotelPin(newActivePin)
setActiveHotelCard(newActivePin)
if (newActivePin === null) {
setHoveredHotelPin(null)
setActiveHotelCard(null)
}
if (activeHotel === pinName || pinName === null) {
deactivate()
return
}
},
[activeHotelPin, setActiveHotelPin, setActiveHotelCard]
)
const handleHover = useCallback(
(pinName: string | null) => {
if (pinName !== null && activeHotelPin !== pinName) {
setHoveredHotelPin(pinName)
if (activeHotelPin && setActiveHotelPin) {
setActiveHotelPin(null)
setActiveHotelCard(null)
}
} else if (pinName === null) {
setHoveredHotelPin(null)
}
activate(pinName)
},
[activeHotelPin, setActiveHotelPin, setActiveHotelCard]
[activeHotel, activate, deactivate]
)
return (
<div>
{hotelPins.map((pin) => {
const isActiveOrHovered =
activeHotelPin === pin.name || hoveredHotelPin === pin.name
activeHotel === pin.name || hoveredHotel === pin.name
const hotelPrice =
pin.memberPrice ?? pin.publicPrice ?? pin.redemptionPrice
return (
@@ -67,8 +44,8 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
position={pin.coordinates}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
zIndex={isActiveOrHovered ? 2 : 0}
onMouseEnter={() => handleHover(pin.name)}
onMouseLeave={() => handleHover(null)}
onMouseEnter={() => engage(pin.name)}
onMouseLeave={() => disengage()}
onClick={() => toggleActiveHotelPin(pin.name)}
>
<div className={styles.dialogContainer}>
@@ -76,37 +53,17 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
isOpen={isActiveOrHovered}
handleClose={(event: { stopPropagation: () => void }) => {
event.stopPropagation()
if (activeHotelPin === pin.name) {
toggleActiveHotelPin(null)
}
deactivate()
disengage()
}}
data={pin}
/>
</div>
<span
className={`${styles.pin} ${isActiveOrHovered ? styles.active : ""}`}
>
<span className={styles.pinIcon}>
<HotelMarker
width={16}
color={isActiveOrHovered ? "burgundy" : "white"}
/>
</span>
<Body
asChild
color={isActiveOrHovered ? "white" : "baseTextHighContrast"}
>
<span>
{/* TODO: Handle when no price is available */}
{hotelPrice
? formatPrice(intl, hotelPrice, pin.currency)
: intl.formatMessage({
defaultMessage: "N/A",
})}
</span>
</Body>
</span>
<HotelPin
isActive={isActiveOrHovered}
hotelPrice={hotelPrice}
currency={pin.currency}
/>
</AdvancedMarker>
)
})}

View File

@@ -10,16 +10,16 @@ import Caption from "@/components/TempDesignSystem/Text/Caption"
import PoiMarker from "../../Markers/Poi"
import ScandicMarker from "../../Markers/Scandic"
import styles from "./hotelMapContent.module.css"
import styles from "./poiMapMarkers.module.css"
import type { HotelMapContentProps } from "@/types/hotel"
import type { PoiMapMarkersProps } from "@/types/hotel"
export default function HotelMapContent({
export default function PoiMapMarkers({
coordinates,
pointsOfInterest,
onActivePoiChange,
activePoi,
}: HotelMapContentProps) {
}: PoiMapMarkersProps) {
const intl = useIntl()
function toggleActivePoi(poiName: string) {

View File

@@ -9,7 +9,7 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Button from "@/components/TempDesignSystem/Button"
import HotelListingMapContent from "./HotelListingMapContent"
import HotelMapContent from "./HotelMapContent"
import PoiMapMarkers from "./PoiMapMarkers"
import styles from "./interactiveMap.module.css"
@@ -36,6 +36,7 @@ export default function InteractiveMap({
disableDefaultUI: true,
clickableIcons: false,
mapId,
gestureHandling: "greedy",
}
function zoomIn() {
@@ -67,7 +68,7 @@ export default function InteractiveMap({
<Map {...mapOptions} onTilesLoaded={onTilesLoaded}>
{hotelPins && <HotelListingMapContent hotelPins={hotelPins} />}
{pointsOfInterest && (
<HotelMapContent
<PoiMapMarkers
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
onActivePoiChange={onActivePoiChange}

View File

@@ -35,7 +35,7 @@
.zoomButtons {
display: grid;
gap: var(--Spacing-x1);
gap: var(--Spacing-x2);
}
.closeButton {
@@ -55,8 +55,8 @@
@media screen and (min-width: 768px) {
.ctaButtons {
top: var(--Spacing-x4);
right: var(--Spacing-x4);
bottom: var(--Spacing-x4);
right: var(--Spacing-x5);
bottom: var(--Spacing-x7);
justify-content: space-between;
}