feat(sw-343) set up intercepted routes for map modal
This commit is contained in:
@@ -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<ElementRef<"dialog">>(null)
|
||||||
|
|
||||||
|
console.log("modal is open")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogRef.current?.open) {
|
||||||
|
dialogRef.current?.showModal()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function onDismiss() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop">
|
||||||
|
<dialog ref={dialogRef} className="modal" onClose={onDismiss}>
|
||||||
|
{children}
|
||||||
|
<button onClick={onDismiss} className="close-button" />
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<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 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 (
|
||||||
|
<MapModal>
|
||||||
|
<SelectHotelMap
|
||||||
|
apiKey={googleMapsApiKey}
|
||||||
|
coordinates={centralCoordinates}
|
||||||
|
pointsOfInterest={pointOfInterests}
|
||||||
|
mapId={googleMapId}
|
||||||
|
/>
|
||||||
|
</MapModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.layout {
|
||||||
|
min-height: 100dvh;
|
||||||
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
|
import { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default function HotelReservationLayout({
|
||||||
|
children,
|
||||||
|
modal,
|
||||||
|
}: React.PropsWithChildren<
|
||||||
|
LayoutArgs<LangParams> & { modal: React.ReactNode }
|
||||||
|
>) {
|
||||||
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.layout}>
|
||||||
|
{children}
|
||||||
|
{modal}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,48 +1,60 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchAvailableHotels,
|
fetchAvailableHotels,
|
||||||
|
generateChildrenString,
|
||||||
getFiltersFromHotels,
|
getFiltersFromHotels,
|
||||||
|
getPointOfInterests,
|
||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
|
||||||
|
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import {
|
import { type SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
PointOfInterest,
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
PointOfInterestCategoryNameEnum,
|
|
||||||
PointOfInterestGroupEnum,
|
|
||||||
} from "@/types/hotel"
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default async function SelectHotelMapPage({
|
export default async function SelectHotelMapPage({
|
||||||
params,
|
params,
|
||||||
}: PageArgs<LangParams, {}>) {
|
searchParams,
|
||||||
|
}: PageArgs<LangParams, SelectHotelSearchParams>) {
|
||||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
||||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
||||||
setLang(params.lang)
|
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({
|
const hotels = await fetchAvailableHotels({
|
||||||
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
cityId: city.id,
|
||||||
roomStayStartDate: "2024-11-02",
|
roomStayStartDate: searchParams.fromDate,
|
||||||
roomStayEndDate: "2024-11-03",
|
roomStayEndDate: searchParams.toDate,
|
||||||
adults: 1,
|
adults,
|
||||||
|
children,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = getFiltersFromHotels(hotels)
|
const filters = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
// TODO: this is just a quick transformation to get something there. May need rework
|
const pointOfInterests = getPointOfInterests(hotels)
|
||||||
const pointOfInterests: PointOfInterest[] = hotels.map((hotel) => ({
|
|
||||||
coordinates: {
|
|
||||||
lat: hotel.hotelData.location.latitude,
|
|
||||||
lng: hotel.hotelData.location.longitude,
|
|
||||||
},
|
|
||||||
name: hotel.hotelData.name,
|
|
||||||
distance: hotel.hotelData.location.distanceToCentre,
|
|
||||||
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
|
||||||
group: PointOfInterestGroupEnum.LOCATION,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
|
||||||
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
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(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
@@ -41,3 +48,33 @@ export function getFiltersFromHotels(hotels: HotelData[]) {
|
|||||||
|
|
||||||
return filterList
|
return filterList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bedTypeMap: Record<number, string> = {
|
||||||
|
[BedTypeEnum.IN_ADULTS_BED]: "ParentsBed",
|
||||||
|
[BedTypeEnum.IN_CRIB]: "Crib",
|
||||||
|
[BedTypeEnum.IN_EXTRA_BED]: "ExtraBed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateChildrenString(children: Child[]): string {
|
||||||
|
return `[${children
|
||||||
|
?.map((child) => {
|
||||||
|
const age = child.age
|
||||||
|
const bedType = bedTypeMap[+child.bed]
|
||||||
|
return `${age}:${bedType}`
|
||||||
|
})
|
||||||
|
.join(",")}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] {
|
||||||
|
// TODO: this is just a quick transformation to get something there. May need rework
|
||||||
|
return hotels.map((hotel) => ({
|
||||||
|
coordinates: {
|
||||||
|
lat: hotel.hotelData.location.latitude,
|
||||||
|
lng: hotel.hotelData.location.longitude,
|
||||||
|
},
|
||||||
|
name: hotel.hotelData.name,
|
||||||
|
distance: hotel.hotelData.location.distanceToCentre,
|
||||||
|
categoryName: PointOfInterestCategoryNameEnum.HOTEL,
|
||||||
|
group: PointOfInterestGroupEnum.LOCATION,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function MobileButtonContainer({ city }: { city: string }) {
|
|||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`${selectHotelMap[lang]}/${city}`}
|
href={`${selectHotelMap[lang]}`}
|
||||||
keepSearchParams
|
keepSearchParams
|
||||||
color="burgundy"
|
color="burgundy"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
components/MapModal/index.tsx
Normal file
43
components/MapModal/index.tsx
Normal file
@@ -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 (
|
||||||
|
<Modal isOpen={true}>
|
||||||
|
<Dialog className={styles.dynamicMap}>
|
||||||
|
{children}
|
||||||
|
<button onClick={onDismiss} className="close-button">
|
||||||
|
close
|
||||||
|
</button>
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
components/MapModal/mapModal.module.css
Normal file
18
components/MapModal/mapModal.module.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user