Merge remote-tracking branch 'origin' into feature/tracking
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing"
|
||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||
|
||||
@@ -8,27 +10,18 @@ import styles from "./hotelListing.module.css"
|
||||
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export default function HotelListing({
|
||||
hotels,
|
||||
activeHotelPin,
|
||||
setActiveHotelPin,
|
||||
}: HotelListingProps) {
|
||||
export default function HotelListing({ hotels }: HotelListingProps) {
|
||||
const { activeHotelPin } = useHotelsMapStore()
|
||||
return (
|
||||
<>
|
||||
<div className={styles.hotelListing}>
|
||||
<HotelCardListing
|
||||
hotelData={hotels}
|
||||
type={HotelCardListingTypeEnum.MapListing}
|
||||
activeCard={activeHotelPin}
|
||||
onHotelCardHover={setActiveHotelPin}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.hotelListingMobile} data-open={!!activeHotelPin}>
|
||||
<HotelCardDialogListing
|
||||
hotels={hotels}
|
||||
activeCard={activeHotelPin}
|
||||
onActiveCardChange={setActiveHotelPin}
|
||||
/>
|
||||
<HotelCardDialogListing hotels={hotels} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getCityCoordinates } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import {
|
||||
fetchAvailableHotels,
|
||||
getFiltersFromHotels,
|
||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
||||
import SelectHotelMap from "."
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import type {
|
||||
HotelData,
|
||||
NullableHotelData,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type {
|
||||
TrackingChannelEnum,
|
||||
TrackingSDKHotelInfo,
|
||||
TrackingSDKPageData} from "@/types/components/tracking";
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
function isValidHotelData(hotel: NullableHotelData): hotel is HotelData {
|
||||
return hotel !== null && hotel !== undefined
|
||||
}
|
||||
|
||||
export async function SelectHotelMapContainer({
|
||||
city,
|
||||
searchParams,
|
||||
adultsInRoom,
|
||||
childrenInRoom,
|
||||
child,
|
||||
}: SelectHotelMapContainerProps) {
|
||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||
|
||||
const fetchAvailableHotelsPromise = safeTry(
|
||||
fetchAvailableHotels({
|
||||
cityId: city.id,
|
||||
roomStayStartDate: searchParams.fromDate,
|
||||
roomStayEndDate: searchParams.toDate,
|
||||
adults: adultsInRoom,
|
||||
children: childrenInRoom,
|
||||
})
|
||||
)
|
||||
|
||||
const [hotels] = await fetchAvailableHotelsPromise
|
||||
|
||||
const validHotels = hotels?.filter(isValidHotelData) || []
|
||||
|
||||
const hotelPins = getHotelPins(validHotels)
|
||||
const filterList = getFiltersFromHotels(validHotels)
|
||||
const cityCoordinates = await getCityCoordinates({
|
||||
city: city.name,
|
||||
hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress },
|
||||
})
|
||||
|
||||
const arrivalDate = new Date(searchParams.fromDate)
|
||||
const departureDate = new Date(searchParams.toDate)
|
||||
|
||||
const pageTrackingData: TrackingSDKPageData = {
|
||||
pageId: "select-hotel",
|
||||
domainLanguage: searchParams.lang as Lang,
|
||||
channel: TrackingChannelEnum["hotelreservation"],
|
||||
pageName: "hotelreservation|select-hotel|mapview",
|
||||
siteSections: "hotelreservation|select-hotel|mapview",
|
||||
pageType: "bookinghotelsmapviewpage",
|
||||
siteVersion: "new-web",
|
||||
}
|
||||
|
||||
const hotelsTrackingData: TrackingSDKHotelInfo = {
|
||||
availableResults: validHotels.length,
|
||||
searchTerm: searchParams.city,
|
||||
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
|
||||
departureDate: format(departureDate, "yyyy-MM-dd"),
|
||||
noOfAdults: adultsInRoom,
|
||||
noOfChildren: child?.length,
|
||||
ageOfChildren: child?.map((c) => c.age).join(","),
|
||||
childBedPreference: child?.map((c) => ChildBedMapEnum[c.bed]).join("|"),
|
||||
noOfRooms: 1, // // TODO: Handle multiple rooms
|
||||
duration: differenceInCalendarDays(departureDate, arrivalDate),
|
||||
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
|
||||
searchType: "destination",
|
||||
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
|
||||
country: validHotels?.[0].hotelData.address.country,
|
||||
region: validHotels?.[0].hotelData.address.city,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectHotelMap
|
||||
apiKey={googleMapsApiKey}
|
||||
hotelPins={hotelPins}
|
||||
mapId={googleMapId}
|
||||
hotels={validHotels}
|
||||
filterList={filterList}
|
||||
cityCoordinates={cityCoordinates}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<TrackingSDK
|
||||
pageData={pageTrackingData}
|
||||
hotelInfo={hotelsTrackingData}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
.container {
|
||||
max-width: var(--max-width);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.listingContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skeletonContainer {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
gap: var(--Spacing-x2);
|
||||
padding-top: var(--Spacing-x6);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.skeletonItem {
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
.mapContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
.listingContainer {
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||
overflow-y: auto;
|
||||
max-width: 505px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.skeletonContainer {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import { RoomCardSkeleton } from "../../SelectRate/RoomSelection/RoomCard/RoomCardSkeleton"
|
||||
|
||||
import styles from "./SelectHotelMapContainerSkeleton.module.css"
|
||||
|
||||
type Props = {
|
||||
count?: number
|
||||
}
|
||||
|
||||
export function SelectHotelMapContainerSkeleton({ count = 2 }: Props) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.listingContainer}>
|
||||
<div className={styles.skeletonContainer}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div key={index} className={styles.skeletonItem}>
|
||||
<RoomCardSkeleton />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mapContainer}>
|
||||
<SkeletonShimmer width={"100%"} height="100%" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
"use client"
|
||||
import { useMap } from "@vis.gl/react-google-maps"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import { RoomCardSkeleton } from "@/components/HotelReservation/SelectRate/RoomSelection/RoomCard/RoomCardSkeleton"
|
||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { debounce } from "@/utils/debounce"
|
||||
|
||||
import FilterAndSortModal from "../../FilterAndSortModal"
|
||||
import HotelListing from "../HotelListing"
|
||||
import { getVisibleHotels } from "./utils"
|
||||
|
||||
import styles from "./selectHotelMapContent.module.css"
|
||||
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
const SKELETON_LOAD_DELAY = 750
|
||||
|
||||
export default function SelectHotelContent({
|
||||
hotelPins,
|
||||
cityCoordinates,
|
||||
mapId,
|
||||
hotels,
|
||||
filterList,
|
||||
}: Omit<SelectHotelMapProps, "apiKey">) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
|
||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelData[]>([])
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
const [showSkeleton, setShowSkeleton] = useState<boolean>(false)
|
||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const { activeHotelCard, activeHotelPin } = useHotelsMapStore()
|
||||
|
||||
const coordinates = useMemo(
|
||||
() =>
|
||||
isAboveMobile
|
||||
? cityCoordinates
|
||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 },
|
||||
[isAboveMobile, cityCoordinates]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (listingContainerRef.current) {
|
||||
const activeElement =
|
||||
listingContainerRef.current.querySelector(`[data-active="true"]`)
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||
}
|
||||
}
|
||||
}, [activeHotelCard, activeHotelPin])
|
||||
|
||||
useEffect(() => {
|
||||
const hotelListingElement = document.querySelector(
|
||||
`.${styles.listingContainer}`
|
||||
)
|
||||
if (!hotelListingElement) return
|
||||
|
||||
const handleScroll = () => {
|
||||
const hasScrolledPast = hotelListingElement.scrollTop > 490
|
||||
setShowBackToTop(hasScrolledPast)
|
||||
}
|
||||
|
||||
hotelListingElement.addEventListener("scroll", handleScroll)
|
||||
return () => hotelListingElement.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
function scrollToTop() {
|
||||
const hotelListingElement = document.querySelector(
|
||||
`.${styles.listingContainer}`
|
||||
)
|
||||
hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
const filteredHotelPins = useMemo(
|
||||
() =>
|
||||
hotelPins.filter((hotel) =>
|
||||
activeFilters.every((filterId) =>
|
||||
hotel.facilityIds.includes(Number(filterId))
|
||||
)
|
||||
),
|
||||
[activeFilters, hotelPins]
|
||||
)
|
||||
|
||||
const getHotelCards = useCallback(() => {
|
||||
const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map)
|
||||
setVisibleHotels(visibleHotels)
|
||||
setTimeout(() => {
|
||||
setShowSkeleton(true)
|
||||
}, SKELETON_LOAD_DELAY)
|
||||
}, [hotels, filteredHotelPins, map])
|
||||
|
||||
/**
|
||||
* Updates visible hotels when map viewport changes (zoom/pan)
|
||||
* - Debounces updates to prevent excessive re-renders during map interaction
|
||||
* - Shows loading skeleton while map tiles load
|
||||
* - Triggers on: initial load, zoom, pan, and tile loading completion
|
||||
*/
|
||||
const debouncedUpdateHotelCards = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
if (!map) return
|
||||
setShowSkeleton(false)
|
||||
getHotelCards()
|
||||
}, 100),
|
||||
[map, getHotelCards]
|
||||
)
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
intent="inverted"
|
||||
size="small"
|
||||
theme="base"
|
||||
className={styles.closeButton}
|
||||
asChild
|
||||
>
|
||||
<Link href={selectHotel(lang)} keepSearchParams>
|
||||
<CloseIcon color="burgundy" />
|
||||
{intl.formatMessage({ id: "Close the map" })}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.listingContainer} ref={listingContainerRef}>
|
||||
<div className={styles.filterContainer}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
variant="icon"
|
||||
wrapping
|
||||
className={styles.filterContainerCloseButton}
|
||||
asChild
|
||||
>
|
||||
<Link href={selectHotel(lang)} keepSearchParams>
|
||||
<CloseLargeIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<FilterAndSortModal filters={filterList} />
|
||||
</div>
|
||||
{showSkeleton ? (
|
||||
<>
|
||||
<RoomCardSkeleton />
|
||||
<RoomCardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<HotelListing hotels={visibleHotels} />
|
||||
)}
|
||||
{showBackToTop && (
|
||||
<BackToTopButton position="left" onClick={scrollToTop} />
|
||||
)}
|
||||
</div>
|
||||
<InteractiveMap
|
||||
closeButton={closeButton}
|
||||
coordinates={coordinates}
|
||||
hotelPins={filteredHotelPins}
|
||||
mapId={mapId}
|
||||
onTilesLoaded={debouncedUpdateHotelCards}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+2
-4
@@ -14,10 +14,6 @@
|
||||
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;
|
||||
@@ -40,6 +36,7 @@
|
||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||
overflow-y: auto;
|
||||
min-width: 420px;
|
||||
width: 420px;
|
||||
position: relative;
|
||||
}
|
||||
.container {
|
||||
@@ -49,5 +46,6 @@
|
||||
.filterContainer {
|
||||
justify-content: flex-end;
|
||||
padding: 0 0 var(--Spacing-x1);
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export function getVisibleHotelPins(
|
||||
map: google.maps.Map | null,
|
||||
filteredHotelPins: HotelPin[]
|
||||
) {
|
||||
if (!map || !filteredHotelPins) return []
|
||||
|
||||
const bounds = map.getBounds()
|
||||
if (!bounds) return []
|
||||
|
||||
return filteredHotelPins.filter((pin) => {
|
||||
const { lat, lng } = pin.coordinates
|
||||
return bounds.contains({ lat, lng })
|
||||
})
|
||||
}
|
||||
|
||||
export function getVisibleHotels(
|
||||
hotels: HotelData[],
|
||||
filteredHotelPins: HotelPin[],
|
||||
map: google.maps.Map | null
|
||||
) {
|
||||
const visibleHotelPins = getVisibleHotelPins(map, filteredHotelPins)
|
||||
const visibleHotels = hotels.filter((hotel) =>
|
||||
visibleHotelPins.some((pin) => pin.operaId === hotel.hotelData.operaId)
|
||||
)
|
||||
return visibleHotels
|
||||
}
|
||||
@@ -1,24 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||
import SelectHotelMapContent from "./SelectHotelMapContent"
|
||||
|
||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import FilterAndSortModal from "../FilterAndSortModal"
|
||||
import HotelListing from "./HotelListing"
|
||||
|
||||
import styles from "./selectHotelMap.module.css"
|
||||
|
||||
import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export default function SelectHotelMap({
|
||||
apiKey,
|
||||
@@ -28,109 +14,15 @@ export default function SelectHotelMap({
|
||||
filterList,
|
||||
cityCoordinates,
|
||||
}: SelectHotelMapProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||
const [activeHotelPin, setActiveHotelPin] = useState<string | null>(null)
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const selectHotelParams = new URLSearchParams(searchParams.toString())
|
||||
const selectedHotel = selectHotelParams.get("selectedHotel")
|
||||
|
||||
const coordinates = isAboveMobile
|
||||
? cityCoordinates
|
||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||
|
||||
useEffect(() => {
|
||||
if (listingContainerRef.current) {
|
||||
const activeElement =
|
||||
listingContainerRef.current.querySelector(`[data-active="true"]`)
|
||||
if (activeElement) {
|
||||
activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||
}
|
||||
}
|
||||
}, [activeHotelPin])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedHotel) {
|
||||
setActiveHotelPin(selectedHotel)
|
||||
}
|
||||
}, [selectedHotel])
|
||||
|
||||
useEffect(() => {
|
||||
const hotelListingElement = document.querySelector(
|
||||
`.${styles.listingContainer}`
|
||||
)
|
||||
if (!hotelListingElement) return
|
||||
|
||||
const handleScroll = () => {
|
||||
const hasScrolledPast = hotelListingElement.scrollTop > 490
|
||||
setShowBackToTop(hasScrolledPast)
|
||||
}
|
||||
|
||||
hotelListingElement.addEventListener("scroll", handleScroll)
|
||||
return () => hotelListingElement.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
function scrollToTop() {
|
||||
const hotelListingElement = document.querySelector(
|
||||
`.${styles.listingContainer}`
|
||||
)
|
||||
hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
function handlePageRedirect() {
|
||||
router.push(`${selectHotel(lang)}?${searchParams.toString()}`)
|
||||
}
|
||||
|
||||
const closeButton = (
|
||||
<Button
|
||||
intent="inverted"
|
||||
size="small"
|
||||
theme="base"
|
||||
className={styles.closeButton}
|
||||
onClick={handlePageRedirect}
|
||||
>
|
||||
<CloseIcon color="burgundy" />
|
||||
{intl.formatMessage({ id: "Close the map" })}
|
||||
</Button>
|
||||
)
|
||||
return (
|
||||
<APIProvider apiKey={apiKey}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.listingContainer} ref={listingContainerRef}>
|
||||
<div className={styles.filterContainer}>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
variant="icon"
|
||||
wrapping
|
||||
onClick={handlePageRedirect}
|
||||
className={styles.filterContainerCloseButton}
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</Button>
|
||||
<FilterAndSortModal filters={filterList} />
|
||||
</div>
|
||||
<HotelListing
|
||||
hotels={hotels}
|
||||
activeHotelPin={activeHotelPin}
|
||||
setActiveHotelPin={setActiveHotelPin}
|
||||
/>
|
||||
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||
</div>
|
||||
<InteractiveMap
|
||||
closeButton={closeButton}
|
||||
coordinates={coordinates}
|
||||
hotelPins={hotelPins}
|
||||
activeHotelPin={activeHotelPin}
|
||||
onActiveHotelPinChange={setActiveHotelPin}
|
||||
mapId={mapId}
|
||||
/>
|
||||
</div>
|
||||
<SelectHotelMapContent
|
||||
hotelPins={hotelPins}
|
||||
cityCoordinates={cityCoordinates}
|
||||
mapId={mapId}
|
||||
hotels={hotels}
|
||||
filterList={filterList}
|
||||
/>
|
||||
</APIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user