Merged in feat/sw-343-select-hotel-map-view-mobile (pull request #808)

feat/sw-343-select-hotel-map-view-mobile

Approved-by: Niclas Edenvin
This commit is contained in:
Pontus Dreij
2024-11-06 09:07:07 +00:00
28 changed files with 501 additions and 99 deletions

View File

@@ -1,5 +1,6 @@
.container {
min-width: 272px;
display: none;
}
.facilities {
@@ -24,3 +25,9 @@
height: 1.25rem;
margin: 0;
}
@media (min-width: 768px) {
.container {
display: block;
}
}

View File

@@ -0,0 +1,48 @@
"use client"
import { useIntl } from "react-intl"
import { selectHotelMap } from "@/constants/routes/hotelReservation"
import { FilterIcon, MapIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import styles from "./mobileMapButtonContainer.module.css"
export default function MobileMapButtonContainer({ city }: { city: string }) {
const intl = useIntl()
const lang = useLang()
return (
<div className={styles.buttonContainer}>
<Button
asChild
variant="icon"
intent="secondary"
size="small"
className={styles.button}
>
<Link
href={`${selectHotelMap[lang]}`}
keepSearchParams
color="burgundy"
>
<MapIcon color="burgundy" />
{intl.formatMessage({ id: "See on map" })}
</Link>
</Button>
{/* TODO: Add filter toggle */}
<Button
variant="icon"
intent="secondary"
size="small"
className={styles.button}
>
<FilterIcon color="burgundy" />
{intl.formatMessage({ id: "Filter and sort" })}
</Button>
</div>
)
}

View File

@@ -0,0 +1,15 @@
.buttonContainer {
display: flex;
gap: var(--Spacing-x2);
margin-bottom: var(--Spacing-x3);
}
.button {
flex: 1;
}
@media (min-width: 768px) {
.buttonContainer {
display: none;
}
}

View File

@@ -0,0 +1,9 @@
.hotelListing {
display: none;
}
@media (min-width: 768px) {
.hotelListing {
display: block;
}
}

View File

@@ -1,5 +1,7 @@
"use client"
import styles from "./hotelListing.module.css"
import { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
// TODO: This component is copied from
@@ -7,5 +9,5 @@ import { HotelListingProps } from "@/types/components/hotelReservation/selectHot
// Look at that for inspiration on how to do the interaction with the map.
export default function HotelListing({}: HotelListingProps) {
return <section>Hotel listing TBI</section>
return <section className={styles.hotelListing}>Hotel listing TBI</section>
}

View File

@@ -1,14 +1,14 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import { useRouter, useSearchParams } from "next/navigation"
import { useState } from "react"
import { useIntl } from "react-intl"
import { selectHotel } from "@/constants/routes/hotelReservation"
import { CloseIcon } from "@/components/Icons"
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
import InteractiveMap from "@/components/Maps/InteractiveMap"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import HotelListing from "./HotelListing"
@@ -22,36 +22,60 @@ export default function SelectHotelMap({
coordinates,
pointsOfInterest,
mapId,
isModal,
}: SelectHotelMapProps) {
const searchParams = useSearchParams()
const router = useRouter()
const lang = useLang()
const intl = useIntl()
const [activePoi, setActivePoi] = useState<string | null>(null)
function handleModalDismiss() {
router.back()
}
function handlePageRedirect() {
router.push(`${selectHotel[lang]}?${searchParams.toString()}`)
}
const closeButton = (
<Button
asChild
intent="inverted"
size="small"
theme="base"
className={styles.closeButton}
onClick={isModal ? handleModalDismiss : handlePageRedirect}
>
<Link href={selectHotel[lang]} keepSearchParams color="burgundy">
<CloseIcon color="burgundy" />
{intl.formatMessage({ id: "Close the map" })}
</Link>
<CloseIcon color="burgundy" />
{intl.formatMessage({ id: "Close the map" })}
</Button>
)
return (
<APIProvider apiKey={apiKey}>
<HotelListing />
<InteractiveMap
closeButton={closeButton}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
activePoi={activePoi}
onActivePoiChange={setActivePoi}
mapId={mapId}
/>
<div className={styles.container}>
<div className={styles.filterContainer}>
<Button
intent="text"
size="small"
variant="icon"
wrapping
onClick={isModal ? handleModalDismiss : handlePageRedirect}
>
<CloseLargeIcon />
</Button>
<span>Filter and sort</span>
{/* TODO: Add filter and sort button */}
</div>
<HotelListing />
<InteractiveMap
closeButton={closeButton}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
activePoi={activePoi}
onActivePoiChange={setActivePoi}
mapId={mapId}
/>
</div>
</APIProvider>
)
}

View File

@@ -2,4 +2,39 @@
pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
display: none !important;
}
.container {
height: 100%;
}
.filterContainer {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
top: 0;
left: 0;
right: 0;
z-index: 10;
background-color: var(--Base-Surface-Secondary-light-Normal);
padding: 0 var(--Spacing-x2);
height: 44px;
}
.filterContainer .closeButton {
color: var(--UI-Text-High-Contrast);
}
@media (min-width: 768px) {
.closeButton {
display: flex !important;
}
.filterContainer {
display: none;
}
.container {
display: flex;
}
}

View File

@@ -0,0 +1,23 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function FilterIcon({ className, color, ...props }: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
{...props}
>
<path
d="M9.58789 15.8125C9.46046 15.8125 9.34813 15.7681 9.25091 15.6792C9.15368 15.5903 9.10507 15.4819 9.10507 15.3542V10.7292L4.33424 4.9375C4.21618 4.78472 4.19886 4.62153 4.28226 4.44792C4.36568 4.27431 4.508 4.1875 4.70924 4.1875H15.2926C15.4938 4.1875 15.6361 4.27431 15.7196 4.44792C15.803 4.62153 15.7856 4.78472 15.6676 4.9375L10.8967 10.7292V15.3542C10.8967 15.4819 10.8489 15.5903 10.7533 15.6792C10.6577 15.7681 10.5462 15.8125 10.4188 15.8125H9.58789ZM10.0009 9.625L13.3134 5.58333H6.66757L10.0009 9.625Z"
fill="#4D001B"
/>
</svg>
)
}

View File

@@ -45,6 +45,7 @@ export { default as ErrorCircleIcon } from "./ErrorCircle"
export { default as EyeHideIcon } from "./EyeHide"
export { default as EyeShowIcon } from "./EyeShow"
export { default as FanIcon } from "./Fan"
export { default as FilterIcon } from "./Filter"
export { default as FitnessIcon } from "./Fitness"
export { default as FootstoolIcon } from "./Footstool"
export { default as GalleryIcon } from "./Gallery"

View File

@@ -0,0 +1,85 @@
"use client"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { Dialog, Modal } from "react-aria-components"
import { debounce } from "@/utils/debounce"
import styles from "./mapModal.module.css"
export function MapModal({ children }: { children: React.ReactNode }) {
const router = useRouter()
const [mapHeight, setMapHeight] = useState("0px")
const [mapTop, setMapTop] = useState("0px")
const [isOpen, setOpen] = useState(true)
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
const rootDiv = useRef<HTMLDivElement | null>(null)
const handleOnOpenChange = (open: boolean) => {
setOpen(open)
if (!open) {
router.back()
}
}
// 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
const scrollY = window.scrollY
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
setMapTop(`${topPosition + scrollY}px`)
}, [])
// 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.
useEffect(() => {
// Skip the first render
if (!rootDiv.current) {
return
}
if (scrollHeightWhenOpened === 0) {
const scrollY = window.scrollY
setScrollHeightWhenOpened(scrollY)
window.scrollTo({ top: 0, behavior: "instant" })
}
}, [scrollHeightWhenOpened, rootDiv])
useEffect(() => {
const debouncedResizeHandler = debounce(function () {
handleMapHeight()
})
const observer = new ResizeObserver(debouncedResizeHandler)
observer.observe(document.documentElement)
return () => {
if (observer) {
observer.unobserve(document.documentElement)
}
}
}, [rootDiv, handleMapHeight])
return (
<div className={styles.wrapper} ref={rootDiv}>
<Modal isDismissable isOpen={isOpen} onOpenChange={handleOnOpenChange}>
<Dialog
style={
{
"--hotel-map-height": mapHeight,
"--hotel-map-top": mapTop,
} as React.CSSProperties
}
className={styles.dynamicMap}
>
{children}
</Dialog>
</Modal>
</div>
)
}

View File

@@ -0,0 +1,18 @@
.dynamicMap {
--hotel-map-height: 100dvh;
--hotel-map-top: 0px;
position: absolute;
top: var(--hotel-map-top);
left: 0;
height: var(--hotel-map-height);
width: 100dvw;
z-index: var(--hotel-dynamic-map-z-index);
display: flex;
flex-direction: column;
background-color: var(--Base-Surface-Primary-light-Normal);
}
.wrapper {
position: absolute;
top: 0;
left: 0;
}

View File

@@ -1,6 +1,7 @@
.mapContainer {
--button-box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}