feat(SW-1957): Mapview on hotel pages is now activated by search params
Approved-by: Matilda Landström
This commit is contained in:
@@ -1,15 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
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 { Dialog, Modal } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import useHotelPageStore from "@/stores/hotel-page"
|
|
||||||
|
|
||||||
import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
import CloseLargeIcon from "@/components/Icons/CloseLarge"
|
||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
|
||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
|
|
||||||
import Sidebar from "./Sidebar"
|
import Sidebar from "./Sidebar"
|
||||||
@@ -26,18 +24,17 @@ export default function DynamicMap({
|
|||||||
mapId,
|
mapId,
|
||||||
}: DynamicMapProps) {
|
}: DynamicMapProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const isMapView = useMemo(
|
||||||
|
() => searchParams.get("view") === "map",
|
||||||
|
[searchParams]
|
||||||
|
)
|
||||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||||
const [mapHeight, setMapHeight] = useState("0px")
|
const [mapHeight, setMapHeight] = useState("0px")
|
||||||
const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore()
|
|
||||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||||
const [activePoi, setActivePoi] = useState<string | null>(null)
|
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)
|
// 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 handleMapHeight = useCallback(() => {
|
||||||
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
|
||||||
@@ -45,6 +42,12 @@ export default function DynamicMap({
|
|||||||
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
|
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,
|
// 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.
|
// 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.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDynamicMapOpen && scrollHeightWhenOpened === 0) {
|
if (isMapView && scrollHeightWhenOpened === 0) {
|
||||||
const scrollY = window.scrollY
|
const scrollY = window.scrollY
|
||||||
setScrollHeightWhenOpened(scrollY)
|
setScrollHeightWhenOpened(scrollY)
|
||||||
window.scrollTo({ top: 0, behavior: "instant" })
|
window.scrollTo({ top: 0, behavior: "instant" })
|
||||||
} else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) {
|
} else if (!isMapView && scrollHeightWhenOpened !== 0) {
|
||||||
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
|
window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
|
||||||
setScrollHeightWhenOpened(0)
|
setScrollHeightWhenOpened(0)
|
||||||
}
|
}
|
||||||
}, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv])
|
}, [isMapView, scrollHeightWhenOpened, rootDiv])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const debouncedResizeHandler = debounce(function () {
|
const debouncedResizeHandler = debounce(function () {
|
||||||
@@ -78,7 +81,7 @@ export default function DynamicMap({
|
|||||||
observer.unobserve(document.documentElement)
|
observer.unobserve(document.documentElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [rootDiv, isDynamicMapOpen, handleMapHeight])
|
}, [rootDiv, isMapView, handleMapHeight])
|
||||||
|
|
||||||
const closeButton = (
|
const closeButton = (
|
||||||
<Button
|
<Button
|
||||||
@@ -87,7 +90,7 @@ export default function DynamicMap({
|
|||||||
variant="icon"
|
variant="icon"
|
||||||
size="small"
|
size="small"
|
||||||
className={styles.closeButton}
|
className={styles.closeButton}
|
||||||
onClick={closeDynamicMap}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
<CloseLargeIcon color="burgundy" />
|
<CloseLargeIcon color="burgundy" />
|
||||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||||
@@ -98,7 +101,7 @@ export default function DynamicMap({
|
|||||||
<APIProvider apiKey={apiKey}>
|
<APIProvider apiKey={apiKey}>
|
||||||
<div className={styles.wrapper} ref={rootDiv}>
|
<div className={styles.wrapper} ref={rootDiv}>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isDynamicMapOpen}
|
isOpen={isMapView}
|
||||||
UNSTABLE_portalContainer={rootDiv.current || undefined}
|
UNSTABLE_portalContainer={rootDiv.current || undefined}
|
||||||
>
|
>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useParams } from "next/navigation"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import useHotelPageStore from "@/stores/hotel-page"
|
|
||||||
|
|
||||||
import PoiMarker from "@/components/Maps/Markers/Poi"
|
import PoiMarker from "@/components/Maps/Markers/Poi"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
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) {
|
export default function MapCard({ hotelName, pois }: MapCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { openDynamicMap } = useHotelPageStore()
|
const params = useParams()
|
||||||
|
const [mapUrl, setMapUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
function handleOpenMapClick() {
|
useEffect(() => {
|
||||||
openDynamicMap()
|
const url = new URL(window.location.href)
|
||||||
trackHotelMapClick()
|
url.searchParams.set("view", "map")
|
||||||
}
|
setMapUrl(url.toString())
|
||||||
|
}, [params])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mapCard}>
|
<div className={styles.mapCard}>
|
||||||
@@ -62,15 +65,13 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Button
|
{mapUrl ? (
|
||||||
theme="base"
|
<Button theme="base" intent="secondary" size="small" fullWidth asChild>
|
||||||
intent="secondary"
|
<Link href={mapUrl} scroll={true} onClick={trackHotelMapClick}>
|
||||||
size="small"
|
{intl.formatMessage({ id: "Explore nearby" })}
|
||||||
fullWidth
|
</Link>
|
||||||
onClick={handleOpenMapClick}
|
</Button>
|
||||||
>
|
) : null}
|
||||||
{intl.formatMessage({ id: "Explore nearby" })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"use client"
|
"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 { 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 { HouseIcon, MapIcon } from "@/components/Icons"
|
||||||
import { trackHotelMapClick } from "@/utils/tracking"
|
import { trackHotelMapClick } from "@/utils/tracking"
|
||||||
@@ -10,40 +13,53 @@ import styles from "./mobileToggle.module.css"
|
|||||||
|
|
||||||
export default function MobileMapToggle() {
|
export default function MobileMapToggle() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { isDynamicMapOpen, openDynamicMap, closeDynamicMap } =
|
const searchParams = useSearchParams()
|
||||||
useHotelPageStore()
|
const params = useParams()
|
||||||
|
const isMapView = useMemo(
|
||||||
|
() => searchParams.get("view") === "map",
|
||||||
|
[searchParams]
|
||||||
|
)
|
||||||
|
const [mapUrl, setMapUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
function handleOpenMapClick() {
|
useEffect(() => {
|
||||||
openDynamicMap()
|
const url = new URL(window.location.href)
|
||||||
trackHotelMapClick()
|
url.searchParams.set("view", "map")
|
||||||
|
setMapUrl(url.toString())
|
||||||
|
}, [params])
|
||||||
|
|
||||||
|
if (!mapUrl) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.mobileToggle}>
|
<div className={styles.mobileToggle}>
|
||||||
<button
|
<span
|
||||||
type="button"
|
className={`${styles.iconWrapper} ${!isMapView ? styles.active : ""}`}
|
||||||
className={`${styles.button} ${!isDynamicMapOpen ? styles.active : ""}`}
|
|
||||||
onClick={closeDynamicMap}
|
|
||||||
>
|
>
|
||||||
<HouseIcon
|
<HouseIcon
|
||||||
color={!isDynamicMapOpen ? "white" : "red"}
|
color={!isMapView ? "white" : "red"}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
/>
|
/>
|
||||||
<span>{intl.formatMessage({ id: "Hotel" })}</span>
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
</button>
|
<span>{intl.formatMessage({ id: "Hotel" })}</span>
|
||||||
<button
|
</Typography>
|
||||||
type="button"
|
</span>
|
||||||
className={`${styles.button} ${isDynamicMapOpen ? styles.active : ""}`}
|
<span
|
||||||
onClick={handleOpenMapClick}
|
className={`${styles.iconWrapper} ${isMapView ? styles.active : ""}`}
|
||||||
>
|
>
|
||||||
<MapIcon
|
<Link
|
||||||
color={isDynamicMapOpen ? "white" : "red"}
|
className={styles.link}
|
||||||
height={24}
|
href={mapUrl}
|
||||||
width={24}
|
scroll={true}
|
||||||
/>
|
onClick={trackHotelMapClick}
|
||||||
<span>{intl.formatMessage({ id: "Map" })}</span>
|
>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
padding: var(--Spacing-x-half);
|
padding: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.iconWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -24,22 +24,24 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 2.5rem;
|
border-radius: 2.5rem;
|
||||||
color: var(--Base-Text-Accent);
|
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);
|
background-color: var(--Base-Surface-Primary-light-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.active {
|
.iconWrapper.active {
|
||||||
background-color: var(--Primary-Strong-Surface-Normal);
|
background-color: var(--Primary-Strong-Surface-Normal);
|
||||||
color: var(--Base-Text-Inverted);
|
color: var(--Base-Text-Inverted);
|
||||||
}
|
}
|
||||||
.button.active:hover {
|
.iconWrapper.active:hover {
|
||||||
background-color: var(--Primary-Strong-Surface-Hover);
|
background-color: var(--Primary-Strong-Surface-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
display: contents;
|
||||||
|
color: var(--Base-Text-Accent);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.mobileToggle {
|
.mobileToggle {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user