diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/modal.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/modal.tsx new file mode 100644 index 000000000..ab8e0a451 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/modal.tsx @@ -0,0 +1,30 @@ +"use client" + +import { useRouter } from "next/navigation" +import { type ElementRef, useEffect, useRef } from "react" + +export function Modal({ children }: { children: React.ReactNode }) { + const router = useRouter() + const dialogRef = useRef>(null) + + console.log("modal is open") + + useEffect(() => { + if (!dialogRef.current?.open) { + dialogRef.current?.showModal() + } + }, []) + + function onDismiss() { + router.back() + } + + return ( +
+ + {children} + +
+ ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx new file mode 100644 index 000000000..6df8e0fec --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -0,0 +1,79 @@ +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, + 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) { + 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 = 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 ( + + + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx new file mode 100644 index 000000000..86b9e9a38 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css new file mode 100644 index 000000000..0969a7151 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css @@ -0,0 +1,4 @@ +.layout { + min-height: 100dvh; + background-color: var(--Base-Background-Primary-Normal); +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx new file mode 100644 index 000000000..ab5f62674 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx @@ -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 & { modal: React.ReactNode } +>) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return ( +
+ {children} + {modal} +
+ ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 8dca8f4c7..4c09e3bd8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1,48 +1,60 @@ +import { notFound } from "next/navigation" + import { env } from "@/env/server" +import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, + generateChildrenString, getFiltersFromHotels, + getPointOfInterests, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" -import { - PointOfInterest, - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" -import { LangParams, PageArgs } from "@/types/params" +import { type SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelMapPage({ params, -}: PageArgs) { + searchParams, +}: PageArgs) { const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY 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 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: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, + cityId: city.id, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults, + children, }) 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, - })) + const pointOfInterests = getPointOfInterests(hotels) return (
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 8dfc76a1d..b14268126 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -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,33 @@ export function getFiltersFromHotels(hotels: HotelData[]) { return filterList } + +const bedTypeMap: Record = { + [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, + })) +} diff --git a/components/HotelReservation/SelectHotel/MobileButtonContainer/index.tsx b/components/HotelReservation/SelectHotel/MobileButtonContainer/index.tsx index 1df90483f..730cd780b 100644 --- a/components/HotelReservation/SelectHotel/MobileButtonContainer/index.tsx +++ b/components/HotelReservation/SelectHotel/MobileButtonContainer/index.tsx @@ -25,7 +25,7 @@ export default function MobileButtonContainer({ city }: { city: string }) { className={styles.button} > diff --git a/components/HotelReservation/SelectHotel/MobileButtonContainer/mobileButtonContainer.module.css b/components/HotelReservation/SelectHotel/MobileButtonContainer/mobileButtonContainer.module.css index e69de29bb..5d0610d8f 100644 --- a/components/HotelReservation/SelectHotel/MobileButtonContainer/mobileButtonContainer.module.css +++ b/components/HotelReservation/SelectHotel/MobileButtonContainer/mobileButtonContainer.module.css @@ -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; + } +} diff --git a/components/MapModal/index.tsx b/components/MapModal/index.tsx new file mode 100644 index 000000000..8064355cb --- /dev/null +++ b/components/MapModal/index.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useEffect, useRef, useState } from "react" +import { Dialog, Modal } from "react-aria-components" + +import styles from "./mapModal.module.css" + +export function MapModal({ children }: { children: React.ReactNode }) { + const router = useRouter() + const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) + const hasMounted = useRef(false) + + function onDismiss() { + router.back() + } + + // Making sure the map is always opened at the top of the page, just below the header. + // When closing, the page should scroll back to the position it was before opening the map. + useEffect(() => { + // Skip the first render + if (!hasMounted.current) { + hasMounted.current = true + return + } + + if (scrollHeightWhenOpened === 0) { + setScrollHeightWhenOpened(window.scrollY) + window.scrollTo({ top: 0, behavior: "instant" }) + } + }, [scrollHeightWhenOpened]) + + return ( + + + {children} + + + + ) +} diff --git a/components/MapModal/mapModal.module.css b/components/MapModal/mapModal.module.css new file mode 100644 index 000000000..31029868e --- /dev/null +++ b/components/MapModal/mapModal.module.css @@ -0,0 +1,18 @@ +.dynamicMap { + position: fixed; + top: calc( + var(--main-menu-mobile-height) + var(--booking-widget-mobile-height) - 4px + ); + right: 0; + bottom: 0; + left: 0; + z-index: var(--dialog-z-index); + display: flex; + background-color: var(--Base-Surface-Primary-light-Normal); +} + +@media screen and (min-width: 768px) { + .dynamicMap { + top: var(--main-menu-desktop-height); + } +}