feat(sw-343) set up intercepted routes for map modal

This commit is contained in:
Pontus Dreij
2024-10-31 09:09:22 +01:00
parent 0aed1d9d57
commit efad3381b6
11 changed files with 291 additions and 26 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -0,0 +1,4 @@
.layout {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
}

View File

@@ -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>
)
}

View File

@@ -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<LangParams, {}>) {
searchParams,
}: PageArgs<LangParams, SelectHotelSearchParams>) {
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 (
<main className={styles.main}>

View File

@@ -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<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,
}))
}

View File

@@ -25,7 +25,7 @@ export default function MobileButtonContainer({ city }: { city: string }) {
className={styles.button}
>
<Link
href={`${selectHotelMap[lang]}/${city}`}
href={`${selectHotelMap[lang]}`}
keepSearchParams
color="burgundy"
>

View File

@@ -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;
}
}

View 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>
)
}

View 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);
}
}