feat(SW-1957): Mapview on hotel pages is now activated by search params

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-03-24 12:50:28 +00:00
parent b0674d07f5
commit 348ae53c1d
5 changed files with 86 additions and 79 deletions

View File

@@ -1,15 +1,13 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import { useCallback, useEffect, useRef, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Dialog, Modal } from "react-aria-components"
import { useIntl } from "react-intl"
import useHotelPageStore from "@/stores/hotel-page"
import CloseLargeIcon from "@/components/Icons/CloseLarge"
import InteractiveMap from "@/components/Maps/InteractiveMap"
import Button from "@/components/TempDesignSystem/Button"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
import { debounce } from "@/utils/debounce"
import Sidebar from "./Sidebar"
@@ -26,18 +24,17 @@ export default function DynamicMap({
mapId,
}: DynamicMapProps) {
const intl = useIntl()
const router = useRouter()
const searchParams = useSearchParams()
const isMapView = useMemo(
() => searchParams.get("view") === "map",
[searchParams]
)
const rootDiv = useRef<HTMLDivElement | null>(null)
const [mapHeight, setMapHeight] = useState("0px")
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
const [activePoi, setActivePoi] = useState<string | null>(null)
useHandleKeyUp((event: KeyboardEvent) => {
if (event.key === "Escape" && isDynamicMapOpen) {
closeDynamicMap()
}
})
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
const handleMapHeight = useCallback(() => {
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
@@ -45,6 +42,12 @@ export default function DynamicMap({
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
}, [])
function handleClose() {
const url = new URL(window.location.href)
url.searchParams.delete("view")
router.push(url.toString())
}
// Making sure the map is always opened at the top of the page,
// just below the header and booking widget as these should stay visible.
// When closing, the page should scroll back to the position it was before opening the map.
@@ -54,15 +57,15 @@ export default function DynamicMap({
return
}
if (isDynamicMapOpen && scrollHeightWhenOpened === 0) {
if (isMapView && scrollHeightWhenOpened === 0) {
const scrollY = window.scrollY
setScrollHeightWhenOpened(scrollY)
window.scrollTo({ top: 0, behavior: "instant" })
} else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) {
} else if (!isMapView && scrollHeightWhenOpened !== 0) {
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
setScrollHeightWhenOpened(0)
}
}, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv])
}, [isMapView, scrollHeightWhenOpened, rootDiv])
useEffect(() => {
const debouncedResizeHandler = debounce(function () {
@@ -78,7 +81,7 @@ export default function DynamicMap({
observer.unobserve(document.documentElement)
}
}
}, [rootDiv, isDynamicMapOpen, handleMapHeight])
}, [rootDiv, isMapView, handleMapHeight])
const closeButton = (
<Button
@@ -87,7 +90,7 @@ export default function DynamicMap({
variant="icon"
size="small"
className={styles.closeButton}
onClick={closeDynamicMap}
onClick={handleClose}
>
<CloseLargeIcon color="burgundy" />
<span>{intl.formatMessage({ id: "Close the map" })}</span>
@@ -98,7 +101,7 @@ export default function DynamicMap({
<APIProvider apiKey={apiKey}>
<div className={styles.wrapper} ref={rootDiv}>
<Modal
isOpen={isDynamicMapOpen}
isOpen={isMapView}
UNSTABLE_portalContainer={rootDiv.current || undefined}
>
<Dialog

View File

@@ -1,9 +1,10 @@
"use client"
import Link from "next/link"
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import useHotelPageStore from "@/stores/hotel-page"
import PoiMarker from "@/components/Maps/Markers/Poi"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
@@ -17,12 +18,14 @@ import type { MapCardProps } from "@/types/components/hotelPage/map/mapCard"
export default function MapCard({ hotelName, pois }: MapCardProps) {
const intl = useIntl()
const { openDynamicMap } = useHotelPageStore()
const params = useParams()
const [mapUrl, setMapUrl] = useState<string | null>(null)
function handleOpenMapClick() {
openDynamicMap()
trackHotelMapClick()
}
useEffect(() => {
const url = new URL(window.location.href)
url.searchParams.set("view", "map")
setMapUrl(url.toString())
}, [params])
return (
<div className={styles.mapCard}>
@@ -62,15 +65,13 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
))}
</ul>
<Button
theme="base"
intent="secondary"
size="small"
fullWidth
onClick={handleOpenMapClick}
>
{intl.formatMessage({ id: "Explore nearby" })}
</Button>
{mapUrl ? (
<Button theme="base" intent="secondary" size="small" fullWidth asChild>
<Link href={mapUrl} scroll={true} onClick={trackHotelMapClick}>
{intl.formatMessage({ id: "Explore nearby" })}
</Link>
</Button>
) : null}
</div>
)
}

View File

@@ -1,7 +1,10 @@
"use client"
import Link from "next/link"
import { useParams, useSearchParams } from "next/navigation"
import { useEffect, useMemo, useState } from "react"
import { useIntl } from "react-intl"
import useHotelPageStore from "@/stores/hotel-page"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { HouseIcon, MapIcon } from "@/components/Icons"
import { trackHotelMapClick } from "@/utils/tracking"
@@ -10,40 +13,53 @@ import styles from "./mobileToggle.module.css"
export default function MobileMapToggle() {
const intl = useIntl()
const { isDynamicMapOpen, openDynamicMap, closeDynamicMap } =
useHotelPageStore()
const searchParams = useSearchParams()
const params = useParams()
const isMapView = useMemo(
() => searchParams.get("view") === "map",
[searchParams]
)
const [mapUrl, setMapUrl] = useState<string | null>(null)
function handleOpenMapClick() {
openDynamicMap()
trackHotelMapClick()
useEffect(() => {
const url = new URL(window.location.href)
url.searchParams.set("view", "map")
setMapUrl(url.toString())
}, [params])
if (!mapUrl) {
return null
}
return (
<div className={styles.mobileToggle}>
<button
type="button"
className={`${styles.button} ${!isDynamicMapOpen ? styles.active : ""}`}
onClick={closeDynamicMap}
<span
className={`${styles.iconWrapper} ${!isMapView ? styles.active : ""}`}
>
<HouseIcon
color={!isDynamicMapOpen ? "white" : "red"}
color={!isMapView ? "white" : "red"}
height={24}
width={24}
/>
<span>{intl.formatMessage({ id: "Hotel" })}</span>
</button>
<button
type="button"
className={`${styles.button} ${isDynamicMapOpen ? styles.active : ""}`}
onClick={handleOpenMapClick}
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{intl.formatMessage({ id: "Hotel" })}</span>
</Typography>
</span>
<span
className={`${styles.iconWrapper} ${isMapView ? styles.active : ""}`}
>
<MapIcon
color={isDynamicMapOpen ? "white" : "red"}
height={24}
width={24}
/>
<span>{intl.formatMessage({ id: "Map" })}</span>
</button>
<Link
className={styles.link}
href={mapUrl}
scroll={true}
onClick={trackHotelMapClick}
>
<MapIcon color={isMapView ? "white" : "red"} height={24} width={24} />
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{intl.formatMessage({ id: "Map" })}</span>
</Typography>
</Link>
</span>
</div>
)
}

View File

@@ -13,7 +13,7 @@
padding: var(--Spacing-x-half);
}
.button {
.iconWrapper {
display: flex;
align-items: center;
justify-content: center;
@@ -24,22 +24,24 @@
cursor: pointer;
border-radius: 2.5rem;
color: var(--Base-Text-Accent);
font-family: var(--typography-Caption-Bold-Desktop-fontFamily);
font-size: var(--typography-Caption-Bold-Desktop-fontSize);
font-weight: var(--typography-Caption-Bold-Desktop-fontWeight);
}
.button:hover {
.iconWrapper:hover {
background-color: var(--Base-Surface-Primary-light-Hover);
}
.button.active {
.iconWrapper.active {
background-color: var(--Primary-Strong-Surface-Normal);
color: var(--Base-Text-Inverted);
}
.button.active:hover {
.iconWrapper.active:hover {
background-color: var(--Primary-Strong-Surface-Hover);
}
.link {
display: contents;
color: var(--Base-Text-Accent);
}
@media screen and (min-width: 1367px) {
.mobileToggle {
display: none;

View File

@@ -1,15 +0,0 @@
import { create } from "zustand"
interface HotelPageState {
isDynamicMapOpen: boolean
openDynamicMap: () => void
closeDynamicMap: () => void
}
const useHotelPageStore = create<HotelPageState>((set) => ({
isDynamicMapOpen: false,
openDynamicMap: () => set({ isDynamicMapOpen: true }),
closeDynamicMap: () => set({ isDynamicMapOpen: false }),
}))
export default useHotelPageStore