Merged in fix/SW-1111-issues-with-map-select-hotel (pull request #1057)
Fix/SW-1111 issues with map select hotel Approved-by: Bianca Widstam
This commit is contained in:
@@ -7,6 +7,7 @@ CMS_API_KEY="test"
|
|||||||
CMS_PREVIEW_TOKEN="test"
|
CMS_PREVIEW_TOKEN="test"
|
||||||
CMS_PREVIEW_URL="test"
|
CMS_PREVIEW_URL="test"
|
||||||
CMS_URL="test"
|
CMS_URL="test"
|
||||||
|
CMS_BRANCH="development"
|
||||||
CURITY_CLIENT_ID_SERVICE="test"
|
CURITY_CLIENT_ID_SERVICE="test"
|
||||||
CURITY_CLIENT_SECRET_SERVICE="test"
|
CURITY_CLIENT_SECRET_SERVICE="test"
|
||||||
CURITY_CLIENT_ID_USER="test"
|
CURITY_CLIENT_ID_USER="test"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
|
|
||||||
export default function LoadingModal() {
|
|
||||||
return <LoadingSpinner />
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { notFound } from "next/navigation"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
import { getCityCoordinates, getLocations } from "@/lib/trpc/memoizedRequests"
|
|
||||||
|
|
||||||
import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils"
|
|
||||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
|
||||||
import {
|
|
||||||
generateChildrenString,
|
|
||||||
getHotelReservationQueryParams,
|
|
||||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
|
||||||
import { MapModal } from "@/components/MapModal"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
|
||||||
|
|
||||||
import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils"
|
|
||||||
|
|
||||||
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
|
||||||
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>) {
|
|
||||||
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 validHotels = hotels.filter(
|
|
||||||
(hotel): hotel is HotelData => hotel !== null
|
|
||||||
)
|
|
||||||
|
|
||||||
const hotelPins = getHotelPins(validHotels)
|
|
||||||
const filterList = getFiltersFromHotels(validHotels)
|
|
||||||
const cityCoordinates = await getCityCoordinates({
|
|
||||||
city: city.name,
|
|
||||||
hotel: { address: hotels?.[0]?.hotelData?.address.streetAddress },
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MapModal>
|
|
||||||
<SelectHotelMap
|
|
||||||
apiKey={googleMapsApiKey}
|
|
||||||
hotelPins={hotelPins}
|
|
||||||
mapId={googleMapId}
|
|
||||||
hotels={validHotels}
|
|
||||||
filterList={filterList}
|
|
||||||
cityCoordinates={cityCoordinates}
|
|
||||||
/>
|
|
||||||
</MapModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function Default() {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -3,3 +3,9 @@
|
|||||||
background-color: var(--Base-Background-Primary-Normal);
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.layout {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,14 +4,6 @@ import { LangParams, LayoutArgs } from "@/types/params"
|
|||||||
|
|
||||||
export default function HotelReservationLayout({
|
export default function HotelReservationLayout({
|
||||||
children,
|
children,
|
||||||
modal,
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
}: React.PropsWithChildren<
|
return <div className={styles.layout}>{children}</div>
|
||||||
LayoutArgs<LangParams> & { modal: React.ReactNode }
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<div className={styles.layout}>
|
|
||||||
{children}
|
|
||||||
{modal}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <SelectHotelMapContainerSkeleton />
|
||||||
|
}
|
||||||
@@ -1 +1,56 @@
|
|||||||
export { default } from "../@modal/(.)map/page"
|
import { notFound } from "next/navigation"
|
||||||
|
import { Suspense } from "react"
|
||||||
|
|
||||||
|
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
|
||||||
|
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
|
||||||
|
import {
|
||||||
|
generateChildrenString,
|
||||||
|
getHotelReservationQueryParams,
|
||||||
|
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
|
import { MapContainer } from "@/components/MapContainer"
|
||||||
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
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 selectHotelParams = new URLSearchParams(searchParams)
|
||||||
|
const selectHotelParamsObject =
|
||||||
|
getHotelReservationQueryParams(selectHotelParams)
|
||||||
|
const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
|
||||||
|
const childrenInRoom = selectHotelParamsObject.room[0].child
|
||||||
|
? generateChildrenString(selectHotelParamsObject.room[0].child)
|
||||||
|
: undefined // TODO: Handle multiple rooms
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.main}>
|
||||||
|
<MapContainer>
|
||||||
|
<SelectHotelMapContainer
|
||||||
|
city={city}
|
||||||
|
searchParams={searchParams}
|
||||||
|
adultsInRoom={adultsInRoom}
|
||||||
|
childrenInRoom={childrenInRoom}
|
||||||
|
/>
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import "@scandic-hotels/design-system/style.css"
|
|||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
import TrpcProvider from "@/lib/trpc/Provider"
|
import TrpcProvider from "@/lib/trpc/Provider"
|
||||||
|
|
||||||
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function List({
|
|||||||
getItemProps={getItemProps}
|
getItemProps={getItemProps}
|
||||||
highlightedIndex={highlightedIndex}
|
highlightedIndex={highlightedIndex}
|
||||||
index={initialIndex + index}
|
index={initialIndex + index}
|
||||||
key={location.id}
|
key={location.id + index}
|
||||||
location={location}
|
location={location}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { SearchProps } from "@/types/components/search"
|
|||||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
const name = "search"
|
const name = "search"
|
||||||
|
|
||||||
export default function Search({ locations }: SearchProps) {
|
export default function Search({ locations }: SearchProps) {
|
||||||
const { register, setValue, trigger, unregister } =
|
const { register, setValue, trigger, unregister } =
|
||||||
useFormContext<BookingWidgetSchema>()
|
useFormContext<BookingWidgetSchema>()
|
||||||
@@ -166,7 +167,6 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
onInputValueChange={(inputValue) => dispatchInputValue(inputValue)}
|
onInputValueChange={(inputValue) => dispatchInputValue(inputValue)}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
closeMenu,
|
|
||||||
getInputProps,
|
getInputProps,
|
||||||
getItemProps,
|
getItemProps,
|
||||||
getLabelProps,
|
getLabelProps,
|
||||||
@@ -207,9 +207,6 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
id: "Destinations & hotels",
|
id: "Destinations & hotels",
|
||||||
}),
|
}),
|
||||||
...register(name, {
|
...register(name, {
|
||||||
onBlur: function () {
|
|
||||||
closeMenu()
|
|
||||||
},
|
|
||||||
onChange: handleOnChange,
|
onChange: handleOnChange,
|
||||||
}),
|
}),
|
||||||
type: "search",
|
type: "search",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { memo, useCallback } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
@@ -32,26 +33,27 @@ function HotelCard({
|
|||||||
hotel,
|
hotel,
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
state = "default",
|
state = "default",
|
||||||
onHotelCardHover,
|
|
||||||
}: HotelCardProps) {
|
}: HotelCardProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const lang = params.lang as Lang
|
const lang = params.lang as Lang
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const { setActiveHotelPin, setActiveHotelCard } = useHotelsMapStore()
|
||||||
|
|
||||||
const { hotelData } = hotel
|
const { hotelData } = hotel
|
||||||
const { price } = hotel
|
const { price } = hotel
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
if (onHotelCardHover && hotelData) {
|
if (hotelData) {
|
||||||
onHotelCardHover(hotelData.name)
|
setActiveHotelPin(hotelData.name)
|
||||||
}
|
}
|
||||||
}, [onHotelCardHover, hotelData])
|
}, [setActiveHotelPin, hotelData])
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
if (onHotelCardHover) {
|
if (hotelData) {
|
||||||
onHotelCardHover(null)
|
setActiveHotelPin(null)
|
||||||
|
setActiveHotelCard(null)
|
||||||
}
|
}
|
||||||
}, [onHotelCardHover])
|
}, [setActiveHotelPin, hotelData, setActiveHotelCard])
|
||||||
|
|
||||||
if (!hotel || !hotelData) return null
|
if (!hotel || !hotelData) return null
|
||||||
|
|
||||||
@@ -96,15 +98,21 @@ function HotelCard({
|
|||||||
{hotelData.address.streetAddress}, {hotelData.address.city}
|
{hotelData.address.streetAddress}, {hotelData.address.city}
|
||||||
</Caption>
|
</Caption>
|
||||||
</address>
|
</address>
|
||||||
<Link
|
<Caption color="baseTextMediumContrast" type="underline" asChild>
|
||||||
className={styles.addressMobile}
|
<Link
|
||||||
href={`${selectHotelMap(lang)}?selectedHotel=${hotelData.name}`}
|
href={`https://www.google.com/maps/dir/?api=1&destination=${hotelData.location.latitude},${hotelData.location.longitude}`}
|
||||||
keepSearchParams
|
className={styles.googleMaps}
|
||||||
>
|
target="_blank"
|
||||||
<Caption color="baseTextMediumContrast" type="underline">
|
aria-label={intl.formatMessage({
|
||||||
|
id: "Driving directions",
|
||||||
|
})}
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: "Driving directions",
|
||||||
|
})}
|
||||||
|
>
|
||||||
{hotelData.address.streetAddress}, {hotelData.address.city}
|
{hotelData.address.streetAddress}, {hotelData.address.city}
|
||||||
</Caption>
|
</Link>
|
||||||
</Link>
|
</Caption>
|
||||||
<div>
|
<div>
|
||||||
<Divider variant="vertical" color="subtle" />
|
<Divider variant="vertical" color="subtle" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -106,8 +106,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.facilities,
|
.facilities {
|
||||||
.memberPrice {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.dialog {
|
.dialog {
|
||||||
|
|||||||
@@ -103,12 +103,14 @@ export default function HotelCardDialog({
|
|||||||
<Caption type="bold">
|
<Caption type="bold">
|
||||||
{intl.formatMessage({ id: "From" })}
|
{intl.formatMessage({ id: "From" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Subtitle type="two">
|
{publicPrice && (
|
||||||
{publicPrice} {currency}
|
<Subtitle type="two">
|
||||||
<Body asChild>
|
{publicPrice} {currency}
|
||||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
<Body asChild>
|
||||||
</Body>
|
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||||
</Subtitle>
|
</Body>
|
||||||
|
</Subtitle>
|
||||||
|
)}
|
||||||
{memberPrice && (
|
{memberPrice && (
|
||||||
<Subtitle
|
<Subtitle
|
||||||
type="two"
|
type="two"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react"
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import useClickOutside from "@/hooks/useClickOutside"
|
import useClickOutside from "@/hooks/useClickOutside"
|
||||||
|
|
||||||
import HotelCardDialog from "../HotelCardDialog"
|
import HotelCardDialog from "../HotelCardDialog"
|
||||||
@@ -14,18 +16,21 @@ import type { HotelCardDialogListingProps } from "@/types/components/hotelReserv
|
|||||||
|
|
||||||
export default function HotelCardDialogListing({
|
export default function HotelCardDialogListing({
|
||||||
hotels,
|
hotels,
|
||||||
activeCard,
|
|
||||||
onActiveCardChange,
|
|
||||||
}: HotelCardDialogListingProps) {
|
}: HotelCardDialogListingProps) {
|
||||||
const hotelsPinData = hotels ? getHotelPins(hotels) : []
|
const hotelsPinData = hotels ? getHotelPins(hotels) : []
|
||||||
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
const activeCardRef = useRef<HTMLDivElement | null>(null)
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||||
const dialogRef = useRef<HTMLDivElement>(null)
|
const dialogRef = useRef<HTMLDivElement>(null)
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)")
|
const isMobile = useMediaQuery("(max-width: 768px)")
|
||||||
|
const { activeHotelCard, setActiveHotelCard, setActiveHotelPin } =
|
||||||
|
useHotelsMapStore()
|
||||||
|
|
||||||
useClickOutside(dialogRef, !!activeCard && isMobile, () => {
|
function handleClose() {
|
||||||
onActiveCardChange(null)
|
setActiveHotelCard(null)
|
||||||
})
|
setActiveHotelPin(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
useClickOutside(dialogRef, !!activeHotelCard && isMobile, handleClose)
|
||||||
|
|
||||||
const handleIntersection = useCallback(
|
const handleIntersection = useCallback(
|
||||||
(entries: IntersectionObserverEntry[]) => {
|
(entries: IntersectionObserverEntry[]) => {
|
||||||
@@ -33,12 +38,12 @@ export default function HotelCardDialogListing({
|
|||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
const cardName = entry.target.getAttribute("data-name")
|
const cardName = entry.target.getAttribute("data-name")
|
||||||
if (cardName) {
|
if (cardName) {
|
||||||
onActiveCardChange(cardName)
|
setActiveHotelCard(cardName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[onActiveCardChange]
|
[setActiveHotelCard]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,13 +78,13 @@ export default function HotelCardDialogListing({
|
|||||||
elements.forEach((el) => observerRef.current?.observe(el))
|
elements.forEach((el) => observerRef.current?.observe(el))
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}, [activeCard])
|
}, [activeHotelCard])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.hotelCardDialogListing} ref={dialogRef}>
|
<div className={styles.hotelCardDialogListing} ref={dialogRef}>
|
||||||
{!!hotelsPinData?.length &&
|
{!!hotelsPinData?.length &&
|
||||||
hotelsPinData.map((data) => {
|
hotelsPinData.map((data) => {
|
||||||
const isActive = data.name === activeCard
|
const isActive = data.name === activeHotelCard
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={data.name}
|
key={data.name}
|
||||||
@@ -88,8 +93,8 @@ export default function HotelCardDialogListing({
|
|||||||
>
|
>
|
||||||
<HotelCardDialog
|
<HotelCardDialog
|
||||||
data={data}
|
data={data}
|
||||||
isOpen={!!activeCard}
|
isOpen={!!activeHotelCard}
|
||||||
handleClose={() => onActiveCardChange(null)}
|
handleClose={handleClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
|||||||
name: hotel.hotelData.name,
|
name: hotel.hotelData.name,
|
||||||
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null,
|
||||||
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null,
|
||||||
currency: hotel.price?.public?.localPrice.currency || null,
|
currency:
|
||||||
|
hotel.price?.public?.localPrice.currency ||
|
||||||
|
hotel.price?.member?.localPrice.currency ||
|
||||||
|
null,
|
||||||
images: [
|
images: [
|
||||||
hotel.hotelData.hotelContent.images,
|
hotel.hotelData.hotelContent.images,
|
||||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
...(hotel.hotelData.gallery?.heroImages ?? []),
|
||||||
@@ -25,5 +28,8 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
|||||||
.slice(0, 3),
|
.slice(0, 3),
|
||||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
||||||
operaId: hotel.hotelData.operaId,
|
operaId: hotel.hotelData.operaId,
|
||||||
|
facilityIds: hotel.hotelData.detailedFacilities.map(
|
||||||
|
(facility) => facility.id
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
@@ -24,14 +25,13 @@ import { AlertTypeEnum } from "@/types/enums/alert"
|
|||||||
export default function HotelCardListing({
|
export default function HotelCardListing({
|
||||||
hotelData,
|
hotelData,
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
activeCard,
|
|
||||||
onHotelCardHover,
|
|
||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const { activeHotelCard } = useHotelsMapStore()
|
||||||
|
|
||||||
const sortBy = useMemo(
|
const sortBy = useMemo(
|
||||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||||
@@ -111,13 +111,16 @@ export default function HotelCardListing({
|
|||||||
hotels.map((hotel) => (
|
hotels.map((hotel) => (
|
||||||
<div
|
<div
|
||||||
key={hotel.hotelData.operaId}
|
key={hotel.hotelData.operaId}
|
||||||
data-active={hotel.hotelData.name === activeCard ? "true" : "false"}
|
data-active={
|
||||||
|
hotel.hotelData.name === activeHotelCard ? "true" : "false"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<HotelCard
|
<HotelCard
|
||||||
hotel={hotel}
|
hotel={hotel}
|
||||||
type={type}
|
type={type}
|
||||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
state={
|
||||||
onHotelCardHover={onHotelCardHover}
|
hotel.hotelData.name === activeHotelCard ? "active" : "default"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -128,7 +131,9 @@ export default function HotelCardListing({
|
|||||||
text={intl.formatMessage({ id: "filters.nohotel.text" })}
|
text={intl.formatMessage({ id: "filters.nohotel.text" })}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
{showBackToTop && (
|
||||||
|
<BackToTopButton position="right" onClick={scrollToTop} />
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
usePathname,
|
||||||
|
useSearchParams,
|
||||||
|
} from "next/dist/client/components/navigation"
|
||||||
|
import { useCallback, useState } from "react"
|
||||||
import {
|
import {
|
||||||
Dialog as AriaDialog,
|
Dialog as AriaDialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -13,25 +18,69 @@ import { useHotelFilterStore } from "@/stores/hotel-filters"
|
|||||||
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Select from "@/components/TempDesignSystem/Select"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
||||||
|
|
||||||
import HotelFilter from "../HotelFilter"
|
import HotelFilter from "../HotelFilter"
|
||||||
import HotelSorter from "../HotelSorter"
|
import { DEFAULT_SORT } from "../HotelSorter"
|
||||||
|
|
||||||
import styles from "./filterAndSortModal.module.css"
|
import styles from "./filterAndSortModal.module.css"
|
||||||
|
|
||||||
import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal"
|
import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal"
|
||||||
|
import {
|
||||||
|
type SortItem,
|
||||||
|
SortOrder,
|
||||||
|
} from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||||
|
|
||||||
export default function FilterAndSortModal({
|
export default function FilterAndSortModal({
|
||||||
filters,
|
filters,
|
||||||
}: FilterAndSortModalProps) {
|
}: FilterAndSortModalProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
useInitializeFiltersFromUrl()
|
useInitializeFiltersFromUrl()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const pathname = usePathname()
|
||||||
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
||||||
const setFilters = useHotelFilterStore((state) => state.setFilters)
|
const setFilters = useHotelFilterStore((state) => state.setFilters)
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
|
const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT)
|
||||||
|
|
||||||
|
const sortItems: SortItem[] = [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: "Distance to city centre" }),
|
||||||
|
value: SortOrder.Distance,
|
||||||
|
},
|
||||||
|
{ label: intl.formatMessage({ id: "Name" }), value: SortOrder.Name },
|
||||||
|
{ label: intl.formatMessage({ id: "Price" }), value: SortOrder.Price },
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: "TripAdvisor rating" }),
|
||||||
|
value: SortOrder.TripAdvisorRating,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSortSelect = useCallback((value: string | number) => {
|
||||||
|
setSort(value.toString())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleApplyFiltersAndSorting = useCallback(
|
||||||
|
(close: () => void) => {
|
||||||
|
if (sort === searchParams.get("sort")) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSearchParams = new URLSearchParams(searchParams)
|
||||||
|
newSearchParams.set("sort", sort)
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`${pathname}?${newSearchParams.toString()}`
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
[pathname, searchParams, sort]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -65,7 +114,16 @@ export default function FilterAndSortModal({
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.sorter}>
|
<div className={styles.sorter}>
|
||||||
<HotelSorter />
|
<Select
|
||||||
|
items={sortItems}
|
||||||
|
defaultSelectedKey={
|
||||||
|
searchParams.get("sort") ?? DEFAULT_SORT
|
||||||
|
}
|
||||||
|
label={intl.formatMessage({ id: "Sort by" })}
|
||||||
|
name="sort"
|
||||||
|
showRadioButton
|
||||||
|
onSelect={handleSortSelect}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.divider}>
|
<div className={styles.divider}>
|
||||||
<Divider color="subtle" />
|
<Divider color="subtle" />
|
||||||
@@ -78,7 +136,7 @@ export default function FilterAndSortModal({
|
|||||||
intent="primary"
|
intent="primary"
|
||||||
size="medium"
|
size="medium"
|
||||||
theme="base"
|
theme="base"
|
||||||
onClick={close}
|
onClick={() => handleApplyFiltersAndSorting(close)}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "See results" },
|
{ id: "See results" },
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing"
|
import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
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 { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
export default function HotelListing({
|
export default function HotelListing({ hotels }: HotelListingProps) {
|
||||||
hotels,
|
const { activeHotelPin } = useHotelsMapStore()
|
||||||
activeHotelPin,
|
|
||||||
setActiveHotelPin,
|
|
||||||
}: HotelListingProps) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.hotelListing}>
|
<div className={styles.hotelListing}>
|
||||||
<HotelCardListing
|
<HotelCardListing
|
||||||
hotelData={hotels}
|
hotelData={hotels}
|
||||||
type={HotelCardListingTypeEnum.MapListing}
|
type={HotelCardListingTypeEnum.MapListing}
|
||||||
activeCard={activeHotelPin}
|
|
||||||
onHotelCardHover={setActiveHotelPin}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.hotelListingMobile} data-open={!!activeHotelPin}>
|
<div className={styles.hotelListingMobile} data-open={!!activeHotelPin}>
|
||||||
<HotelCardDialogListing
|
<HotelCardDialogListing hotels={hotels} />
|
||||||
hotels={hotels}
|
|
||||||
activeCard={activeHotelPin}
|
|
||||||
onActiveCardChange={setActiveHotelPin}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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 { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
|
import { getHotelPins } from "../../HotelCardDialogListing/utils"
|
||||||
|
import SelectHotelMap from "."
|
||||||
|
|
||||||
|
import type {
|
||||||
|
HotelData,
|
||||||
|
NullableHotelData,
|
||||||
|
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
|
import type { SelectHotelMapContainerProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
|
function isValidHotelData(hotel: NullableHotelData): hotel is HotelData {
|
||||||
|
return hotel !== null && hotel !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SelectHotelMapContainer({
|
||||||
|
city,
|
||||||
|
searchParams,
|
||||||
|
adultsInRoom,
|
||||||
|
childrenInRoom,
|
||||||
|
}: 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 },
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectHotelMap
|
||||||
|
apiKey={googleMapsApiKey}
|
||||||
|
hotelPins={hotelPins}
|
||||||
|
mapId={googleMapId}
|
||||||
|
hotels={validHotels}
|
||||||
|
filterList={filterList}
|
||||||
|
cityCoordinates={cityCoordinates}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 async 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { APIProvider } from "@vis.gl/react-google-maps"
|
import { APIProvider } from "@vis.gl/react-google-maps"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { useMediaQuery } from "usehooks-ts"
|
import { useMediaQuery } from "usehooks-ts"
|
||||||
|
|
||||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||||
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import FilterAndSortModal from "../FilterAndSortModal"
|
import FilterAndSortModal from "../FilterAndSortModal"
|
||||||
@@ -28,17 +31,13 @@ export default function SelectHotelMap({
|
|||||||
filterList,
|
filterList,
|
||||||
cityCoordinates,
|
cityCoordinates,
|
||||||
}: SelectHotelMapProps) {
|
}: SelectHotelMapProps) {
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const router = useRouter()
|
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
const isAboveMobile = useMediaQuery("(min-width: 768px)")
|
||||||
const [activeHotelPin, setActiveHotelPin] = useState<string | null>(null)
|
|
||||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||||
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
const listingContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||||
const selectHotelParams = new URLSearchParams(searchParams.toString())
|
const { activeHotelCard, activeHotelPin } = useHotelsMapStore()
|
||||||
const selectedHotel = selectHotelParams.get("selectedHotel")
|
|
||||||
|
|
||||||
const coordinates = isAboveMobile
|
const coordinates = isAboveMobile
|
||||||
? cityCoordinates
|
? cityCoordinates
|
||||||
@@ -52,13 +51,7 @@ export default function SelectHotelMap({
|
|||||||
activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
activeElement.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [activeHotelPin])
|
}, [activeHotelCard, activeHotelPin])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedHotel) {
|
|
||||||
setActiveHotelPin(selectedHotel)
|
|
||||||
}
|
|
||||||
}, [selectedHotel])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hotelListingElement = document.querySelector(
|
const hotelListingElement = document.querySelector(
|
||||||
@@ -82,9 +75,15 @@ export default function SelectHotelMap({
|
|||||||
hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" })
|
hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageRedirect() {
|
const filteredHotelPins = useMemo(
|
||||||
router.push(`${selectHotel(lang)}?${searchParams.toString()}`)
|
() =>
|
||||||
}
|
hotelPins.filter((hotel) =>
|
||||||
|
activeFilters.every((filterId) =>
|
||||||
|
hotel.facilityIds.includes(Number(filterId))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[activeFilters, hotelPins]
|
||||||
|
)
|
||||||
|
|
||||||
const closeButton = (
|
const closeButton = (
|
||||||
<Button
|
<Button
|
||||||
@@ -92,10 +91,12 @@ export default function SelectHotelMap({
|
|||||||
size="small"
|
size="small"
|
||||||
theme="base"
|
theme="base"
|
||||||
className={styles.closeButton}
|
className={styles.closeButton}
|
||||||
onClick={handlePageRedirect}
|
asChild
|
||||||
>
|
>
|
||||||
<CloseIcon color="burgundy" />
|
<Link href={selectHotel(lang)} keepSearchParams>
|
||||||
{intl.formatMessage({ id: "Close the map" })}
|
<CloseIcon color="burgundy" />
|
||||||
|
{intl.formatMessage({ id: "Close the map" })}
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
@@ -108,26 +109,24 @@ export default function SelectHotelMap({
|
|||||||
size="small"
|
size="small"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
wrapping
|
wrapping
|
||||||
onClick={handlePageRedirect}
|
|
||||||
className={styles.filterContainerCloseButton}
|
className={styles.filterContainerCloseButton}
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
<CloseLargeIcon />
|
<Link href={selectHotel(lang)} keepSearchParams>
|
||||||
|
<CloseLargeIcon />
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<FilterAndSortModal filters={filterList} />
|
<FilterAndSortModal filters={filterList} />
|
||||||
</div>
|
</div>
|
||||||
<HotelListing
|
<HotelListing hotels={hotels} />
|
||||||
hotels={hotels}
|
{showBackToTop && (
|
||||||
activeHotelPin={activeHotelPin}
|
<BackToTopButton position="left" onClick={scrollToTop} />
|
||||||
setActiveHotelPin={setActiveHotelPin}
|
)}
|
||||||
/>
|
|
||||||
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
|
||||||
</div>
|
</div>
|
||||||
<InteractiveMap
|
<InteractiveMap
|
||||||
closeButton={closeButton}
|
closeButton={closeButton}
|
||||||
coordinates={coordinates}
|
coordinates={coordinates}
|
||||||
hotelPins={hotelPins}
|
hotelPins={filteredHotelPins}
|
||||||
activeHotelPin={activeHotelPin}
|
|
||||||
onActiveHotelPinChange={setActiveHotelPin}
|
|
||||||
mapId={mapId}
|
mapId={mapId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||||
padding: 0 var(--Spacing-x2);
|
padding: 0 var(--Spacing-x2);
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -49,5 +45,6 @@
|
|||||||
.filterContainer {
|
.filterContainer {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 0 0 var(--Spacing-x1);
|
padding: 0 0 var(--Spacing-x1);
|
||||||
|
position: static;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,58 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { Dialog, Modal } from "react-aria-components"
|
|
||||||
|
|
||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
|
|
||||||
import styles from "./mapModal.module.css"
|
import styles from "./mapModal.module.css"
|
||||||
|
|
||||||
export function MapModal({ children }: { children: React.ReactNode }) {
|
export function MapContainer({ children }: { children: React.ReactNode }) {
|
||||||
const router = useRouter()
|
|
||||||
const [mapHeight, setMapHeight] = useState("0px")
|
const [mapHeight, setMapHeight] = useState("0px")
|
||||||
const [mapTop, setMapTop] = useState("0px")
|
const [mapTop, setMapTop] = useState("0px")
|
||||||
const [isOpen, setIsOpen] = useState(true)
|
const [mapZIndex, setMapZIndex] = useState(0)
|
||||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||||
|
|
||||||
const rootDiv = useRef<HTMLDivElement | null>(null)
|
const rootDiv = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const handleOnOpenChange = (open: boolean) => {
|
|
||||||
setIsOpen(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)
|
// 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
|
||||||
const scrollY = window.scrollY
|
const scrollY = window.scrollY
|
||||||
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
|
setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
|
||||||
setMapTop(`${topPosition + scrollY}px`)
|
setMapTop(`${topPosition + scrollY}px`)
|
||||||
|
setMapZIndex(11)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const originalOverflowY = document.body.style.overflowY
|
||||||
|
|
||||||
|
// Function to enforce overflowY to hidden
|
||||||
|
const enforceOverflowHidden = () => {
|
||||||
|
if (document.body.style.overflowY !== "hidden") {
|
||||||
|
document.body.style.overflowY = "hidden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set overflowY to hidden initially
|
||||||
|
enforceOverflowHidden()
|
||||||
|
|
||||||
|
// Create a MutationObserver to watch for changes to the style attribute
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
enforceOverflowHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe changes to the style attribute of the body
|
||||||
|
observer.observe(document.body, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Disconnect the observer on cleanup
|
||||||
|
observer.disconnect()
|
||||||
|
// Restore the original overflowY style
|
||||||
|
document.body.style.overflowY = originalOverflowY
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -66,19 +89,18 @@ export function MapModal({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper} ref={rootDiv}>
|
<div className={styles.wrapper} ref={rootDiv}>
|
||||||
<Modal isOpen={isOpen} onOpenChange={handleOnOpenChange}>
|
<div
|
||||||
<Dialog
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"--hotel-map-height": mapHeight,
|
||||||
"--hotel-map-height": mapHeight,
|
"--hotel-map-top": mapTop,
|
||||||
"--hotel-map-top": mapTop,
|
"--hotel-dynamic-map-z-index": mapZIndex,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
className={styles.dynamicMap}
|
className={styles.dynamicMap}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Dialog>
|
</div>
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
.dynamicMap {
|
.dynamicMap {
|
||||||
--hotel-map-height: 100dvh;
|
--hotel-map-height: 100dvh;
|
||||||
--hotel-map-top: 0px;
|
--hotel-map-top: 145px;
|
||||||
position: absolute;
|
--hotel-dynamic-map-z-index: 2;
|
||||||
|
position: fixed;
|
||||||
top: var(--hotel-map-top);
|
top: var(--hotel-map-top);
|
||||||
left: 0;
|
left: 0;
|
||||||
height: var(--hotel-map-height);
|
height: var(--hotel-map-height);
|
||||||
@@ -15,4 +16,6 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import {
|
|||||||
AdvancedMarker,
|
AdvancedMarker,
|
||||||
AdvancedMarkerAnchorPoint,
|
AdvancedMarkerAnchorPoint,
|
||||||
} from "@vis.gl/react-google-maps"
|
} from "@vis.gl/react-google-maps"
|
||||||
import { memo, useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
|
|
||||||
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog"
|
import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
@@ -13,38 +15,39 @@ import styles from "./hotelListingMapContent.module.css"
|
|||||||
|
|
||||||
import type { HotelListingMapContentProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelListingMapContentProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
function HotelListingMapContent({
|
function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
|
||||||
activeHotelPin,
|
|
||||||
hotelPins,
|
|
||||||
onActiveHotelPinChange,
|
|
||||||
}: HotelListingMapContentProps) {
|
|
||||||
const [hoveredHotelPin, setHoveredHotelPin] = useState<string | null>(null)
|
const [hoveredHotelPin, setHoveredHotelPin] = useState<string | null>(null)
|
||||||
|
const { activeHotelPin, setActiveHotelPin, setActiveHotelCard } =
|
||||||
|
useHotelsMapStore()
|
||||||
|
|
||||||
const toggleActiveHotelPin = useCallback(
|
const toggleActiveHotelPin = useCallback(
|
||||||
(pinName: string | null) => {
|
(pinName: string | null) => {
|
||||||
if (onActiveHotelPinChange) {
|
if (setActiveHotelPin) {
|
||||||
const newActivePin = activeHotelPin === pinName ? null : pinName
|
const newActivePin = activeHotelPin === pinName ? null : pinName
|
||||||
onActiveHotelPinChange(newActivePin)
|
setActiveHotelPin(newActivePin)
|
||||||
|
setActiveHotelCard(newActivePin)
|
||||||
if (newActivePin === null) {
|
if (newActivePin === null) {
|
||||||
setHoveredHotelPin(null)
|
setHoveredHotelPin(null)
|
||||||
|
setActiveHotelCard(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeHotelPin, onActiveHotelPinChange]
|
[activeHotelPin, setActiveHotelPin, setActiveHotelCard]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleHover = useCallback(
|
const handleHover = useCallback(
|
||||||
(pinName: string | null) => {
|
(pinName: string | null) => {
|
||||||
if (pinName !== null && activeHotelPin !== pinName) {
|
if (pinName !== null && activeHotelPin !== pinName) {
|
||||||
setHoveredHotelPin(pinName)
|
setHoveredHotelPin(pinName)
|
||||||
if (activeHotelPin && onActiveHotelPinChange) {
|
if (activeHotelPin && setActiveHotelPin) {
|
||||||
onActiveHotelPinChange(null)
|
setActiveHotelPin(null)
|
||||||
|
setActiveHotelCard(null)
|
||||||
}
|
}
|
||||||
} else if (pinName === null) {
|
} else if (pinName === null) {
|
||||||
setHoveredHotelPin(null)
|
setHoveredHotelPin(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeHotelPin, onActiveHotelPinChange]
|
[activeHotelPin, setActiveHotelPin, setActiveHotelCard]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,6 +87,7 @@ function HotelListingMapContent({
|
|||||||
color={isActiveOrHovered ? "burgundy" : "white"}
|
color={isActiveOrHovered ? "burgundy" : "white"}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Body
|
<Body
|
||||||
asChild
|
asChild
|
||||||
color={isActiveOrHovered ? "white" : "baseTextHighContrast"}
|
color={isActiveOrHovered ? "white" : "baseTextHighContrast"}
|
||||||
@@ -100,4 +104,4 @@ function HotelListingMapContent({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(HotelListingMapContent)
|
export default HotelListingMapContent
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ export default function InteractiveMap({
|
|||||||
pointsOfInterest,
|
pointsOfInterest,
|
||||||
activePoi,
|
activePoi,
|
||||||
hotelPins,
|
hotelPins,
|
||||||
activeHotelPin,
|
|
||||||
mapId,
|
mapId,
|
||||||
closeButton,
|
closeButton,
|
||||||
onActivePoiChange,
|
onActivePoiChange,
|
||||||
onActiveHotelPinChange,
|
|
||||||
}: InteractiveMapProps) {
|
}: InteractiveMapProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
@@ -50,13 +48,7 @@ export default function InteractiveMap({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
<Map {...mapOptions}>
|
<Map {...mapOptions}>
|
||||||
{hotelPins && (
|
{hotelPins && <HotelListingMapContent hotelPins={hotelPins} />}
|
||||||
<HotelListingMapContent
|
|
||||||
activeHotelPin={activeHotelPin}
|
|
||||||
hotelPins={hotelPins}
|
|
||||||
onActiveHotelPinChange={onActiveHotelPinChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{pointsOfInterest && (
|
{pointsOfInterest && (
|
||||||
<HotelMapContent
|
<HotelMapContent
|
||||||
coordinates={coordinates}
|
coordinates={coordinates}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.sitewideAlert {
|
.sitewideAlert {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
z-index: var(--header-z-index);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alarm {
|
.alarm {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
color: var(--Base-Button-Secondary-On-Fill-Normal);
|
color: var(--Base-Button-Secondary-On-Fill-Normal);
|
||||||
@@ -28,6 +27,14 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.backToTopButtonText {
|
.backToTopButtonText {
|
||||||
display: initial;
|
display: initial;
|
||||||
|
|||||||
@@ -5,12 +5,23 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { ArrowUpIcon } from "@/components/Icons"
|
import { ArrowUpIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import { backToTopButtonVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./backToTopButton.module.css"
|
import styles from "./backToTopButton.module.css"
|
||||||
|
|
||||||
export function BackToTopButton({ onClick }: { onClick: () => void }) {
|
export function BackToTopButton({
|
||||||
|
onClick,
|
||||||
|
position,
|
||||||
|
}: {
|
||||||
|
onClick: () => void
|
||||||
|
position: "left" | "right"
|
||||||
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<ButtonRAC className={styles.backToTopButton} onPress={onClick}>
|
<ButtonRAC
|
||||||
|
className={backToTopButtonVariants({ position })}
|
||||||
|
onPress={onClick}
|
||||||
|
>
|
||||||
<ArrowUpIcon color="burgundy" />
|
<ArrowUpIcon color="burgundy" />
|
||||||
<span className={styles.backToTopButtonText}>
|
<span className={styles.backToTopButtonText}>
|
||||||
{intl.formatMessage({ id: "Back to top" })}
|
{intl.formatMessage({ id: "Back to top" })}
|
||||||
|
|||||||
15
components/TempDesignSystem/BackToTopButton/variants.ts
Normal file
15
components/TempDesignSystem/BackToTopButton/variants.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./backToTopButton.module.css"
|
||||||
|
|
||||||
|
export const backToTopButtonVariants = cva(styles.backToTopButton, {
|
||||||
|
variants: {
|
||||||
|
position: {
|
||||||
|
left: styles.left,
|
||||||
|
right: styles.right,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
})
|
||||||
15
stores/hotels-map.ts
Normal file
15
stores/hotels-map.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
interface HotelsMapState {
|
||||||
|
activeHotelCard: string | null
|
||||||
|
activeHotelPin: string | null
|
||||||
|
setActiveHotelCard: (hotelCard: string | null) => void
|
||||||
|
setActiveHotelPin: (hotelPin: string | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHotelsMapStore = create<HotelsMapState>((set) => ({
|
||||||
|
activeHotelCard: null,
|
||||||
|
activeHotelPin: null,
|
||||||
|
setActiveHotelCard: (hotelCard) => set({ activeHotelCard: hotelCard }),
|
||||||
|
setActiveHotelPin: (hotelPin) => set({ activeHotelPin: hotelPin }),
|
||||||
|
}))
|
||||||
@@ -11,9 +11,7 @@ export interface InteractiveMapProps {
|
|||||||
pointsOfInterest?: PointOfInterest[]
|
pointsOfInterest?: PointOfInterest[]
|
||||||
activePoi?: PointOfInterest["name"] | null
|
activePoi?: PointOfInterest["name"] | null
|
||||||
hotelPins?: HotelPin[]
|
hotelPins?: HotelPin[]
|
||||||
activeHotelPin?: HotelPin["name"] | null
|
|
||||||
mapId: string
|
mapId: string
|
||||||
closeButton: ReactElement
|
closeButton: ReactElement
|
||||||
onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void
|
onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void
|
||||||
onActiveHotelPinChange?: (hotelPin: HotelPin["name"] | null) => void
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ export enum HotelCardListingTypeEnum {
|
|||||||
export type HotelCardListingProps = {
|
export type HotelCardListingProps = {
|
||||||
hotelData: HotelData[]
|
hotelData: HotelData[]
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
activeCard?: string | null
|
|
||||||
onHotelCardHover?: (hotelName: string | null) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HotelData = {
|
export type HotelData = {
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ export type HotelCardProps = {
|
|||||||
hotel: HotelData
|
hotel: HotelData
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
state?: "default" | "active"
|
state?: "default" | "active"
|
||||||
onHotelCardHover?: (hotelName: string | null) => void
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
|
|
||||||
import { HotelData } from "./hotelCardListingProps"
|
import { HotelData } from "./hotelCardListingProps"
|
||||||
import { CategorizedFilters, Filter } from "./hotelFilters"
|
import { CategorizedFilters, Filter } from "./hotelFilters"
|
||||||
|
import { SelectHotelSearchParams } from "./selectHotelSearchParams"
|
||||||
|
|
||||||
import type { Coordinates } from "@/types/components/maps/coordinates"
|
import type { Coordinates } from "@/types/components/maps/coordinates"
|
||||||
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
export interface HotelListingProps {
|
export interface HotelListingProps {
|
||||||
hotels: HotelData[]
|
hotels: HotelData[]
|
||||||
activeHotelPin?: string | null
|
|
||||||
setActiveHotelPin: (hotelName: string | null) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectHotelMapProps {
|
export interface SelectHotelMapProps {
|
||||||
@@ -41,12 +41,11 @@ export type HotelPin = {
|
|||||||
amenities: Filter[]
|
amenities: Filter[]
|
||||||
ratings: number | null
|
ratings: number | null
|
||||||
operaId: string
|
operaId: string
|
||||||
|
facilityIds: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelListingMapContentProps {
|
export interface HotelListingMapContentProps {
|
||||||
activeHotelPin?: HotelPin["name"] | null
|
|
||||||
hotelPins: HotelPin[]
|
hotelPins: HotelPin[]
|
||||||
onActiveHotelPinChange?: (pinName: string | null) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelCardDialogProps {
|
export interface HotelCardDialogProps {
|
||||||
@@ -57,6 +56,11 @@ export interface HotelCardDialogProps {
|
|||||||
|
|
||||||
export interface HotelCardDialogListingProps {
|
export interface HotelCardDialogListingProps {
|
||||||
hotels: HotelData[] | null
|
hotels: HotelData[] | null
|
||||||
activeCard: string | null | undefined
|
}
|
||||||
onActiveCardChange: (hotelName: string | null) => void
|
|
||||||
|
export type SelectHotelMapContainerProps = {
|
||||||
|
city: Location
|
||||||
|
searchParams: SelectHotelSearchParams
|
||||||
|
adultsInRoom: number
|
||||||
|
childrenInRoom: string | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user