Merged in feat/refactor-select-rate (pull request #1402)

Select-rate: refactor - converted RoomsContainer into a client component

* feat/select-rate - refactor and fixed duplicate key warning

* Rooms as client component

* Fixed lang in input

* It works

* Cleanup

* Cleanup

* PR fixes


Approved-by: Joakim Jäderberg
This commit is contained in:
Linus Flood
2025-02-25 08:40:36 +00:00
parent cf3268bda3
commit e2749f5593
11 changed files with 226 additions and 273 deletions

View File

@@ -86,6 +86,7 @@ export default async function DetailsPage({
hotelId: booking.hotelId, hotelId: booking.hotelId,
packageCodes: room.packages, packageCodes: room.packages,
startDate: booking.fromDate, startDate: booking.fromDate,
lang,
}) })
: null : null

View File

@@ -6,7 +6,7 @@ type Props = {
count?: number count?: number
} }
export async function RoomsContainerSkeleton({ count = 4 }: Props) { export function RoomsContainerSkeleton({ count = 4 }: Props) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.filterContainer}></div> <div className={styles.filterContainer}></div>

View File

@@ -1,109 +1,59 @@
"use client"
import { useSession } from "next-auth/react"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import {
getHotel,
getPackages,
getRoomsAvailability,
} from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth" import useLang from "@/hooks/useLang"
import { generateChildrenString } from "@/components/HotelReservation/utils"
import RatesProvider from "@/providers/RatesProvider" import RatesProvider from "@/providers/RatesProvider"
import { isValidSession } from "@/utils/session" import { isValidClientSession } from "@/utils/clientSession"
import { combineRoomAvailabilities } from "../utils" import { useHotelPackages, useRoomsAvailability } from "../utils"
import RateSummary from "./RateSummary" import RateSummary from "./RateSummary"
import Rooms from "./Rooms" import Rooms from "./Rooms"
import { RoomsContainerSkeleton } from "./RoomsContainerSkeleton"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer" import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages"
export function preload( export function RoomsContainer({
hotelId: string,
lang: Lang,
fromDate: string,
toDate: string,
adults: number[],
children?: Child[]
) {
void getHotel({ hotelId, isCardOnlyPayment: false, language: lang })
void getPackages({
adults: adults[0],
children: children ? children?.length : undefined,
endDate: toDate,
hotelId,
packageCodes: [
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
],
startDate: fromDate,
})
const uniqueAdultsCount = Array.from(new Set(adults))
uniqueAdultsCount.forEach((adultsInRoom) => {
void getRoomsAvailability({
adults: adultsInRoom,
children: children ? generateChildrenString(children) : undefined,
hotelId: +hotelId,
roomStayEndDate: toDate,
roomStayStartDate: fromDate,
})
})
}
export async function RoomsContainer({
adultArray, adultArray,
booking, booking,
childArray, childArray,
fromDate, fromDate,
hotelId, hotelId,
lang, hotelData,
toDate, toDate,
}: RoomsContainerProps) { }: RoomsContainerProps) {
const session = await auth() const { data: session } = useSession()
const isUserLoggedIn = isValidSession(session) const isUserLoggedIn = isValidClientSession(session)
const lang = useLang()
const fromDateString = dt(fromDate).format("YYYY-MM-DD") const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD") const toDateString = dt(toDate).format("YYYY-MM-DD")
const hotelData = await getHotel({
hotelId: hotelId.toString(),
isCardOnlyPayment: false,
language: lang,
})
const uniqueAdultsCount = Array.from(new Set(adultArray)) const uniqueAdultsCount = Array.from(new Set(adultArray))
const roomsAvailabilityResults = await Promise.allSettled(
uniqueAdultsCount.map((adultCount) => const { isPending: isLoadingAvailability, data: roomsAvailability } =
getRoomsAvailability({ useRoomsAvailability(
adults: adultCount, uniqueAdultsCount,
hotelId: hotelId, hotelId,
roomStayEndDate: toDateString, fromDateString,
roomStayStartDate: fromDateString, toDateString,
children: lang,
childArray && childArray.length > 0 childArray
? generateChildrenString(childArray)
: undefined,
})
) )
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
adultArray,
childArray,
fromDateString,
toDateString,
hotelId,
lang
) )
const roomsAvailability = combineRoomAvailabilities(roomsAvailabilityResults) if (isLoadingAvailability || isLoadingPackages) {
return <RoomsContainerSkeleton />
const packages = await getPackages({ }
adults: adultArray[0],
children: childArray ? childArray.length : undefined,
endDate: toDateString,
hotelId: hotelId.toString(),
packageCodes: [
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
],
startDate: fromDateString,
})
if (!hotelData?.hotel) { if (!hotelData?.hotel) {
return null return null

View File

@@ -10,11 +10,7 @@ import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreserva
import HotelInfoCard, { import HotelInfoCard, {
HotelInfoCardSkeleton, HotelInfoCardSkeleton,
} from "@/components/HotelReservation/SelectRate/HotelInfoCard" } from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import { import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
preload,
RoomsContainer,
} from "@/components/HotelReservation/SelectRate/RoomsContainer"
import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { convertSearchParamsToObj } from "@/utils/url" import { convertSearchParamsToObj } from "@/utils/url"
@@ -45,15 +41,6 @@ export default async function SelectRatePage({
selectHotelParams.toDate selectHotelParams.toDate
) )
preload(
hotel.id,
params.lang,
fromDate.format("YYYY-MM-DD"),
toDate.format("YYYY-MM-DD"),
adultsInRoom,
childrenInRoom
)
const hotelData = await getHotel({ const hotelData = await getHotel({
hotelId: hotel.id, hotelId: hotel.id,
isCardOnlyPayment: false, isCardOnlyPayment: false,
@@ -104,18 +91,17 @@ export default async function SelectRatePage({
<HotelInfoCard hotelData={hotelData} /> <HotelInfoCard hotelData={hotelData} />
</Suspense> </Suspense>
<Suspense key={suspenseKey} fallback={<RoomsContainerSkeleton />}> <RoomsContainer
<RoomsContainer hotelData={hotelData}
adultArray={adultsInRoom} adultArray={adultsInRoom}
booking={booking} booking={booking}
childArray={childrenInRoom} childArray={childrenInRoom}
fromDate={arrivalDate} fromDate={arrivalDate}
hotelId={hotelId} hotelId={hotelId}
lang={params.lang} toDate={departureDate}
toDate={departureDate} />
/>
</Suspense> <Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<Suspense key={suspenseKey} fallback={null}>
<TrackingSDK <TrackingSDK
pageData={pageTrackingData} pageData={pageTrackingData}
hotelInfo={hotelsTrackingData} hotelInfo={hotelsTrackingData}

View File

@@ -1,4 +1,9 @@
import { trpc } from "@/lib/trpc/client"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { Lang } from "@/constants/languages"
export function combineRoomAvailabilities( export function combineRoomAvailabilities(
availabilityResults: PromiseSettledResult<RoomsAvailability | null>[] availabilityResults: PromiseSettledResult<RoomsAvailability | null>[]
@@ -37,3 +42,56 @@ export function getRates(
), ),
} }
} }
export function useRoomsAvailability(
uniqueAdultsCount: number[],
hotelId: number,
fromDateString: string,
toDateString: string,
lang: Lang,
childArray?: Child[]
) {
const returnValue =
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
hotelId,
roomStayStartDate: fromDateString,
roomStayEndDate: toDateString,
uniqueAdultsCount,
childArray,
lang,
})
const combinedAvailability = returnValue.data?.length
? combineRoomAvailabilities(
returnValue.data as PromiseSettledResult<RoomsAvailability | null>[]
)
: null
return {
...returnValue,
data: combinedAvailability,
}
}
export function useHotelPackages(
adultArray: number[],
childArray: Child[] | undefined,
fromDateString: string,
toDateString: string,
hotelId: number,
lang: Lang
) {
return trpc.hotel.packages.get.useQuery({
adults: adultArray[0], // Using the first adult count
children: childArray ? childArray.length : undefined,
endDate: toDateString,
hotelId: hotelId.toString(),
packageCodes: [
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
],
startDate: fromDateString,
lang: lang,
})
}

View File

@@ -15,7 +15,6 @@ import type {
import type { Lang } from "@/constants/languages" import type { Lang } from "@/constants/languages"
import type { import type {
GetHotelsByCSFilterInput, GetHotelsByCSFilterInput,
GetRoomsAvailabilityInput,
GetSelectedRoomAvailabilityInput, GetSelectedRoomAvailabilityInput,
} from "@/server/routers/hotels/input" } from "@/server/routers/hotels/input"
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input" import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
@@ -87,12 +86,6 @@ export const getHotelPage = cache(async function getMemoizedHotelPage() {
return serverClient().contentstack.hotelPage.get() return serverClient().contentstack.hotelPage.get()
}) })
export const getRoomsAvailability = cache(
async function getMemoizedRoomAvailability(input: GetRoomsAvailabilityInput) {
return serverClient().hotel.availability.rooms(input)
}
)
export const getSelectedRoomAvailability = cache( export const getSelectedRoomAvailability = cache(
function getMemoizedSelectedRoomAvailability( function getMemoizedSelectedRoomAvailability(
input: GetSelectedRoomAvailabilityInput input: GetSelectedRoomAvailabilityInput

View File

@@ -2,6 +2,7 @@ import { z } from "zod"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { Country } from "@/types/enums/country" import { Country } from "@/types/enums/country"
@@ -23,14 +24,22 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
bookingCode: z.string().optional().default(""), bookingCode: z.string().optional().default(""),
}) })
export const roomsAvailabilityInputSchema = z.object({ export const roomsCombinedAvailabilityInputSchema = z.object({
hotelId: z.number(), hotelId: z.number(),
roomStayStartDate: z.string(), roomStayStartDate: z.string(),
roomStayEndDate: z.string(), roomStayEndDate: z.string(),
adults: z.number(), uniqueAdultsCount: z.array(z.number()),
children: z.string().optional(), childArray: z
.array(
z.object({
bed: z.nativeEnum(ChildBedMapEnum),
age: z.number(),
})
)
.optional(),
bookingCode: z.string().optional(), bookingCode: z.string().optional(),
rateCode: z.string().optional(), rateCode: z.string().optional(),
lang: z.nativeEnum(Lang),
}) })
export const selectedRoomAvailabilityInputSchema = z.object({ export const selectedRoomAvailabilityInputSchema = z.object({
@@ -49,10 +58,6 @@ export type GetSelectedRoomAvailabilityInput = z.input<
typeof selectedRoomAvailabilityInputSchema typeof selectedRoomAvailabilityInputSchema
> >
export type GetRoomsAvailabilityInput = z.input<
typeof roomsAvailabilityInputSchema
>
export const ratesInputSchema = z.object({ export const ratesInputSchema = z.object({
hotelId: z.string(), hotelId: z.string(),
}) })
@@ -109,6 +114,7 @@ export const roomPackagesInputSchema = z.object({
adults: z.number(), adults: z.number(),
children: z.number().optional().default(0), children: z.number().optional().default(0),
packageCodes: z.array(z.string()).optional().default([]), packageCodes: z.array(z.string()).optional().default([]),
lang: z.nativeEnum(Lang),
}) })
export const cityCoordinatesInputSchema = z.object({ export const cityCoordinatesInputSchema = z.object({
city: z.string(), city: z.string(),

View File

@@ -70,10 +70,14 @@ export const metrics = {
fail: meter.createCounter("trpc.hotel.packages.get-fail"), fail: meter.createCounter("trpc.hotel.packages.get-fail"),
success: meter.createCounter("trpc.hotel.packages.get-success"), success: meter.createCounter("trpc.hotel.packages.get-success"),
}, },
roomAvailability: { roomsCombinedAvailability: {
counter: meter.createCounter("trpc.hotel.availability.rooms"), counter: meter.createCounter("trpc.hotel.roomsCombinedAvailability.rooms"),
fail: meter.createCounter("trpc.hotel.availability.rooms-fail"), fail: meter.createCounter(
success: meter.createCounter("trpc.hotel.availability.rooms-success"), "trpc.hotel.roomsCombinedAvailability.rooms-fail"
),
success: meter.createCounter(
"trpc.hotel.roomsCombinedAvailability.rooms-success"
),
}, },
selectedRoomAvailability: { selectedRoomAvailability: {
counter: meter.createCounter("trpc.hotel.availability.room"), counter: meter.createCounter("trpc.hotel.availability.room"),

View File

@@ -35,7 +35,7 @@ import {
nearbyHotelIdsInput, nearbyHotelIdsInput,
ratesInputSchema, ratesInputSchema,
roomPackagesInputSchema, roomPackagesInputSchema,
roomsAvailabilityInputSchema, roomsCombinedAvailabilityInputSchema,
selectedRoomAvailabilityInputSchema, selectedRoomAvailabilityInputSchema,
} from "./input" } from "./input"
import { metrics } from "./metrics" import { metrics } from "./metrics"
@@ -470,128 +470,98 @@ export const hotelQueryRouter = router({
const apiLang = toApiLang(lang) const apiLang = toApiLang(lang)
return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken) return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
}), }),
rooms: serviceProcedure
.input(roomsAvailabilityInputSchema) roomsCombinedAvailability: serviceProcedure
.input(roomsCombinedAvailabilityInputSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const { lang } = ctx const { lang } = input
const apiLang = toApiLang(lang) const apiLang = toApiLang(lang)
const { const {
hotelId, hotelId,
roomStayStartDate, roomStayStartDate,
roomStayEndDate, roomStayEndDate,
adults, uniqueAdultsCount,
children, childArray,
bookingCode, bookingCode,
rateCode, rateCode,
} = input } = input
const params: Record<string, string | number | undefined> = { const metricsData = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: apiLang,
}
metrics.roomAvailability.counter.add(1, {
hotelId, hotelId,
roomStayStartDate, roomStayStartDate,
roomStayEndDate, roomStayEndDate,
adults, uniqueAdultsCount,
children, childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode, bookingCode,
}) }
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
console.info( console.info(
"api.hotels.roomsAvailability start", "api.hotels.roomsCombinedAvailability start",
JSON.stringify({ query: { hotelId, params } }) JSON.stringify({ query: { hotelId, params: metricsData } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
) )
if (!apiResponse.ok) { const availabilityResponses = await Promise.allSettled(
const text = await apiResponse.text() uniqueAdultsCount.map(async (adultCount: number) => {
metrics.roomAvailability.fail.add(1, { const params: Record<string, string | number | undefined> = {
hotelId, roomStayStartDate,
roomStayStartDate, roomStayEndDate,
roomStayEndDate, adults: adultCount,
adults, ...(childArray &&
children, childArray.length > 0 && {
bookingCode, children: childArray.join(","),
error_type: "http_error", }),
error: JSON.stringify({ ...(bookingCode && { bookingCode }),
status: apiResponse.status, language: apiLang,
statusText: apiResponse.statusText, }
text,
}), const apiResponse = await api.get(
}) api.endpoints.v1.Availability.hotel(hotelId.toString()),
console.error( {
"api.hotels.roomsAvailability error", headers: {
JSON.stringify({ Authorization: `Bearer ${ctx.serviceToken}`,
query: { hotelId, params }, },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}, },
}) params
) )
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData = if (!apiResponse.ok) {
roomsAvailabilitySchema.safeParse(apiJson) const text = await apiResponse.text()
if (!validateAvailabilityData.success) { metrics.roomsCombinedAvailability.fail.add(1, metricsData)
metrics.roomAvailability.fail.add(1, { console.error("Failed API call", { params, text })
hotelId, return { error: "http_error", details: text }
roomStayStartDate, }
roomStayEndDate,
adults, const apiJson = await apiResponse.json()
children,
bookingCode, const validateAvailabilityData =
error_type: "validation_error", roomsAvailabilitySchema.safeParse(apiJson)
error: JSON.stringify(validateAvailabilityData.error),
}) if (!validateAvailabilityData.success) {
console.error( console.error("Validation error", {
"api.hotels.roomsAvailability validation error", params,
JSON.stringify({ error: validateAvailabilityData.error,
query: { hotelId, params }, })
error: validateAvailabilityData.error, metrics.roomsCombinedAvailability.fail.add(1, metricsData)
}) return {
) error: "validation_error",
return null details: validateAvailabilityData.error,
} }
metrics.roomAvailability.success.add(1, { }
hotelId,
roomStayStartDate, if (rateCode) {
roomStayEndDate, validateAvailabilityData.data.mustBeGuaranteed =
adults, validateAvailabilityData.data.rateDefinitions.find(
children, (rate) => rate.rateCode === rateCode
bookingCode, )?.mustBeGuaranteed
}) }
console.info(
"api.hotels.roomsAvailability success", return validateAvailabilityData.data
JSON.stringify({
query: { hotelId, params: params },
}) })
) )
metrics.roomsCombinedAvailability.success.add(1, metricsData)
if (rateCode) { return availabilityResponses
validateAvailabilityData.data.mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.filter(
(rate) => rate.rateCode === rateCode
)[0].mustBeGuaranteed
}
return validateAvailabilityData.data
}), }),
room: serviceProcedure room: serviceProcedure
.input(selectedRoomAvailabilityInputSchema) .input(selectedRoomAvailabilityInputSchema)
@@ -885,40 +855,35 @@ export const hotelQueryRouter = router({
}), }),
}), }),
rates: router({ rates: router({
get: publicProcedure get: publicProcedure.input(ratesInputSchema).query(async ({}) => {
.input(ratesInputSchema) // TODO: Do a real API call when the endpoint is ready
.query(async ({ input, ctx }) => { // const { hotelId } = input
// TODO: Do a real API call when the endpoint is ready
// const { hotelId } = input
// const params = new URLSearchParams() // const params = new URLSearchParams()
// const apiLang = toApiLang(language) // const apiLang = toApiLang(language)
// params.set("hotelId", hotelId.toString()) // params.set("hotelId", hotelId.toString())
// params.set("language", apiLang) // params.set("language", apiLang)
console.info("api.hotels.rates start", JSON.stringify({})) console.info("api.hotels.rates start", JSON.stringify({}))
const validatedHotelData = ratesSchema.safeParse(tempRatesData) const validatedHotelData = ratesSchema.safeParse(tempRatesData)
if (!tempRatesData) { if (!tempRatesData) {
console.error( console.error("api.hotels.rates error", JSON.stringify({ error: null }))
"api.hotels.rates error", //Can't return null here since consuming component does not handle null yet
JSON.stringify({ error: null }) // return null
) }
//Can't return null here since consuming component does not handle null yet if (!validatedHotelData.success) {
// return null console.error(
} "api.hotels.rates validation error",
if (!validatedHotelData.success) { JSON.stringify({
console.error( error: validatedHotelData.error,
"api.hotels.rates validation error", })
JSON.stringify({ )
error: validatedHotelData.error, throw badRequestError()
}) }
) console.info("api.hotels.rates success", JSON.stringify({}))
throw badRequestError() return validatedHotelData.data
} }),
console.info("api.hotels.rates success", JSON.stringify({}))
return validatedHotelData.data
}),
}), }),
get: serviceProcedure get: serviceProcedure
.input(hotelInputSchema) .input(hotelInputSchema)
@@ -1544,7 +1509,7 @@ export const hotelQueryRouter = router({
const { hotelId, startDate, endDate, adults, children, packageCodes } = const { hotelId, startDate, endDate, adults, children, packageCodes } =
input input
const { lang } = ctx const { lang } = input
const apiLang = toApiLang(lang) const apiLang = toApiLang(lang)

View File

@@ -35,16 +35,6 @@ export const hotelsByHotelIdAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id-fail" "trpc.hotel.availability.hotels-by-hotel-id-fail"
) )
export const roomsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.rooms"
)
export const roomsAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.rooms-success"
)
export const roomsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.rooms-fail"
)
export const selectedRoomAvailabilityCounter = meter.createCounter( export const selectedRoomAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.room" "trpc.hotel.availability.room"
) )

View File

@@ -1,4 +1,4 @@
import type { Lang } from "@/constants/languages" import type { HotelData } from "@/types/hotel"
import type { Child, SelectRateSearchParams } from "./selectRate" import type { Child, SelectRateSearchParams } from "./selectRate"
export interface RoomsContainerProps { export interface RoomsContainerProps {
@@ -7,6 +7,6 @@ export interface RoomsContainerProps {
childArray?: Child[] childArray?: Child[]
fromDate: Date fromDate: Date
hotelId: number hotelId: number
lang: Lang
toDate: Date toDate: Date
hotelData: HotelData | null
} }