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:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user