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:
@@ -0,0 +1,75 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { MapModal } from "@/components/MapModal"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
generateChildrenString,
|
||||
getCentralCoordinates,
|
||||
getPointOfInterests,
|
||||
} from "../../utils"
|
||||
|
||||
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||
import type { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function SelectHotelMapPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<LangParams, SelectHotelSearchParams>) {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
setLang(params.lang)
|
||||
const locations = await getLocations()
|
||||
|
||||
if (!locations || "error" in locations) {
|
||||
return null
|
||||
}
|
||||
const city = locations.data.find(
|
||||
(location) =>
|
||||
location.name.toLowerCase() === searchParams.city.toLowerCase()
|
||||
)
|
||||
if (!city) return notFound()
|
||||
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
|
||||
const selectHotelParams = new URLSearchParams(searchParams)
|
||||
const selectHotelParamsObject =
|
||||
getHotelReservationQueryParams(selectHotelParams)
|
||||
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||
const children = selectHotelParamsObject.room[0].child
|
||||
? generateChildrenString(selectHotelParamsObject.room[0].child)
|
||||
: undefined // TODO: Handle multiple rooms
|
||||
|
||||
const hotels = await fetchAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: searchParams.fromDate,
|
||||
roomStayEndDate: searchParams.toDate,
|
||||
adults,
|
||||
children,
|
||||
})
|
||||
|
||||
const pointOfInterests = getPointOfInterests(hotels)
|
||||
|
||||
const centralCoordinates = getCentralCoordinates(pointOfInterests)
|
||||
|
||||
return (
|
||||
<MapModal>
|
||||
<SelectHotelMap
|
||||
apiKey={googleMapsApiKey}
|
||||
coordinates={centralCoordinates}
|
||||
pointsOfInterest={pointOfInterests}
|
||||
mapId={googleMapId}
|
||||
isModal={true}
|
||||
/>
|
||||
</MapModal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Default() {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.layout {
|
||||
min-height: 100dvh;
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
position: relative;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export default function HotelReservationLayout({
|
||||
children,
|
||||
modal,
|
||||
}: React.PropsWithChildren<
|
||||
LayoutArgs<LangParams> & { modal: React.ReactNode }
|
||||
>) {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
{modal}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
display: grid;
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
min-height: 100dvh;
|
||||
grid-template-columns: 420px 1fr;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,58 +1 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./page.module.css"
|
||||
|
||||
import {
|
||||
PointOfInterest,
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function SelectHotelMapPage({
|
||||
params,
|
||||
}: PageArgs<LangParams, {}>) {
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
setLang(params.lang)
|
||||
|
||||
const hotels = await fetchAvailableHotels({
|
||||
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
||||
roomStayStartDate: "2024-11-02",
|
||||
roomStayEndDate: "2024-11-03",
|
||||
adults: 1,
|
||||
})
|
||||
|
||||
const filters = getFiltersFromHotels(hotels)
|
||||
|
||||
// TODO: this is just a quick transformation to get something there. May need rework
|
||||
const pointOfInterests: PointOfInterest[] = hotels.map((hotel) => ({
|
||||
coordinates: {
|
||||
lat: hotel.hotelData.location.latitude,
|
||||
lng: hotel.hotelData.location.longitude,
|
||||
},
|
||||
name: hotel.hotelData.name,
|
||||
distance: hotel.hotelData.location.distanceToCentre,
|
||||
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
||||
group: PointOfInterestGroupEnum.LOCATION,
|
||||
}))
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<SelectHotelMap
|
||||
apiKey={googleMapsApiKey}
|
||||
// TODO: use correct coordinates. The city center?
|
||||
coordinates={{ lat: 59.32, lng: 18.01 }}
|
||||
pointsOfInterest={pointOfInterests}
|
||||
mapId={googleMapId}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
export { default } from "../@modal/(.)map/page"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.main {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x4);
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4);
|
||||
background-color: var(--Scandic-Brand-Warm-White);
|
||||
min-height: 100dvh;
|
||||
@@ -19,8 +19,28 @@
|
||||
padding: var(--Spacing-x2) var(--Spacing-x0);
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mapContainer {
|
||||
display: block;
|
||||
}
|
||||
.main {
|
||||
flex-direction: row;
|
||||
}
|
||||
.buttonContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||
import {
|
||||
generateChildrenString,
|
||||
getHotelReservationQueryParams,
|
||||
@@ -62,25 +63,28 @@ export default async function SelectHotelPage({
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<section className={styles.section}>
|
||||
<Link href={selectHotelMap[params.lang]} keepSearchParams>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
</Link>
|
||||
<div className={styles.mapContainer}>
|
||||
<Link href={selectHotelMap[params.lang]} keepSearchParams>
|
||||
<StaticMap
|
||||
city={searchParams.city}
|
||||
width={340}
|
||||
height={180}
|
||||
zoomLevel={11}
|
||||
mapType="roadmap"
|
||||
altText={`Map of ${searchParams.city} city center`}
|
||||
/>
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.link}
|
||||
color="burgundy"
|
||||
href={selectHotelMap[params.lang]}
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "Show map" })}
|
||||
<ChevronRightIcon color="burgundy" />
|
||||
</Link>
|
||||
</div>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<HotelFilter filters={filterList} />
|
||||
</section>
|
||||
<HotelCardListing hotelData={hotels} />
|
||||
|
||||
@@ -2,9 +2,16 @@ import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import {
|
||||
type PointOfInterest,
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
|
||||
export async function fetchAvailableHotels(
|
||||
input: AvailabilityInput
|
||||
@@ -41,3 +48,49 @@ export function getFiltersFromHotels(hotels: HotelData[]) {
|
||||
|
||||
return filterList
|
||||
}
|
||||
|
||||
const bedTypeMap: Record<number, string> = {
|
||||
[BedTypeEnum.IN_ADULTS_BED]: "ParentsBed",
|
||||
[BedTypeEnum.IN_CRIB]: "Crib",
|
||||
[BedTypeEnum.IN_EXTRA_BED]: "ExtraBed",
|
||||
}
|
||||
|
||||
export function generateChildrenString(children: Child[]): string {
|
||||
return `[${children
|
||||
?.map((child) => {
|
||||
const age = child.age
|
||||
const bedType = bedTypeMap[+child.bed]
|
||||
return `${age}:${bedType}`
|
||||
})
|
||||
.join(",")}]`
|
||||
}
|
||||
|
||||
export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] {
|
||||
// TODO: this is just a quick transformation to get something there. May need rework
|
||||
return hotels.map((hotel) => ({
|
||||
coordinates: {
|
||||
lat: hotel.hotelData.location.latitude,
|
||||
lng: hotel.hotelData.location.longitude,
|
||||
},
|
||||
name: hotel.hotelData.name,
|
||||
distance: hotel.hotelData.location.distanceToCentre,
|
||||
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
||||
group: PointOfInterestGroupEnum.LOCATION,
|
||||
}))
|
||||
}
|
||||
|
||||
export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) {
|
||||
const centralCoordinates = pointOfInterests.reduce(
|
||||
(acc, poi) => {
|
||||
acc.lat += poi.coordinates.lat
|
||||
acc.lng += poi.coordinates.lng
|
||||
return acc
|
||||
},
|
||||
{ lat: 0, lng: 0 }
|
||||
)
|
||||
|
||||
centralCoordinates.lat /= pointOfInterests.length
|
||||
centralCoordinates.lng /= pointOfInterests.length
|
||||
|
||||
return centralCoordinates
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.hotelListing {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hotelListing {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
23
components/Icons/Filter.tsx
Normal file
23
components/Icons/Filter.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
85
components/MapModal/index.tsx
Normal file
85
components/MapModal/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
18
components/MapModal/mapModal.module.css
Normal file
18
components/MapModal/mapModal.module.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
"See all photos": "Se alle billeder",
|
||||
"See hotel details": "Se hoteloplysninger",
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
"See on map": "Se på kort",
|
||||
"See room details": "Se værelsesdetaljer",
|
||||
"See rooms": "Se værelser",
|
||||
"Select a country": "Vælg et land",
|
||||
|
||||
@@ -285,6 +285,7 @@
|
||||
"See all photos": "Alle Fotos ansehen",
|
||||
"See hotel details": "Hotelinformationen ansehen",
|
||||
"See less FAQ": "Weniger anzeigen FAQ",
|
||||
"See on map": "Karte ansehen",
|
||||
"See room details": "Zimmerdetails ansehen",
|
||||
"See rooms": "Zimmer ansehen",
|
||||
"Select a country": "Wähle ein Land",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"See all photos": "See all photos",
|
||||
"See hotel details": "See hotel details",
|
||||
"See less FAQ": "See less FAQ",
|
||||
"See on map": "See on map",
|
||||
"See room details": "See room details",
|
||||
"See rooms": "See rooms",
|
||||
"Select a country": "Select a country",
|
||||
|
||||
@@ -287,6 +287,7 @@
|
||||
"See all photos": "Katso kaikki kuvat",
|
||||
"See hotel details": "Katso hotellin tiedot",
|
||||
"See less FAQ": "Katso vähemmän UKK",
|
||||
"See on map": "Näytä kartalla",
|
||||
"See room details": "Katso huoneen tiedot",
|
||||
"See rooms": "Katso huoneet",
|
||||
"Select a country": "Valitse maa",
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"See all photos": "Se alle bilder",
|
||||
"See hotel details": "Se hotellinformasjon",
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
"See on map": "Se på kart",
|
||||
"See room details": "Se detaljer om rommet",
|
||||
"See rooms": "Se rom",
|
||||
"Select a country": "Velg et land",
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"See all photos": "Se alla foton",
|
||||
"See hotel details": "Se hotellinformation",
|
||||
"See less FAQ": "See färre FAQ",
|
||||
"See on map": "Se på karta",
|
||||
"See room details": "Se rumsdetaljer",
|
||||
"See rooms": "Se rum",
|
||||
"Select a country": "Välj ett land",
|
||||
|
||||
@@ -12,4 +12,5 @@ export interface SelectHotelMapProps {
|
||||
coordinates: Coordinates
|
||||
pointsOfInterest: PointOfInterest[]
|
||||
mapId: string
|
||||
isModal: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user