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,
packageCodes: room.packages,
startDate: booking.fromDate,
lang,
})
: null

View File

@@ -6,7 +6,7 @@ type Props = {
count?: number
}
export async function RoomsContainerSkeleton({ count = 4 }: Props) {
export function RoomsContainerSkeleton({ count = 4 }: Props) {
return (
<div className={styles.container}>
<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 {
getHotel,
getPackages,
getRoomsAvailability,
} from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import { generateChildrenString } from "@/components/HotelReservation/utils"
import useLang from "@/hooks/useLang"
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 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 { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages"
export function preload(
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({
export function RoomsContainer({
adultArray,
booking,
childArray,
fromDate,
hotelId,
lang,
hotelData,
toDate,
}: RoomsContainerProps) {
const session = await auth()
const isUserLoggedIn = isValidSession(session)
const { data: session } = useSession()
const isUserLoggedIn = isValidClientSession(session)
const lang = useLang()
const fromDateString = dt(fromDate).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 roomsAvailabilityResults = await Promise.allSettled(
uniqueAdultsCount.map((adultCount) =>
getRoomsAvailability({
adults: adultCount,
hotelId: hotelId,
roomStayEndDate: toDateString,
roomStayStartDate: fromDateString,
children:
childArray && childArray.length > 0
? generateChildrenString(childArray)
: undefined,
})
const { isPending: isLoadingAvailability, data: roomsAvailability } =
useRoomsAvailability(
uniqueAdultsCount,
hotelId,
fromDateString,
toDateString,
lang,
childArray
)
const { data: packages, isPending: isLoadingPackages } = useHotelPackages(
adultArray,
childArray,
fromDateString,
toDateString,
hotelId,
lang
)
const roomsAvailability = combineRoomAvailabilities(roomsAvailabilityResults)
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 (isLoadingAvailability || isLoadingPackages) {
return <RoomsContainerSkeleton />
}
if (!hotelData?.hotel) {
return null

View File

@@ -10,11 +10,7 @@ import { getHotelSearchDetails } from "@/app/[lang]/(live)/(public)/hotelreserva
import HotelInfoCard, {
HotelInfoCardSkeleton,
} from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import {
preload,
RoomsContainer,
} from "@/components/HotelReservation/SelectRate/RoomsContainer"
import { RoomsContainerSkeleton } from "@/components/HotelReservation/SelectRate/RoomsContainer/RoomsContainerSkeleton"
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/RoomsContainer"
import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext"
import { convertSearchParamsToObj } from "@/utils/url"
@@ -45,15 +41,6 @@ export default async function SelectRatePage({
selectHotelParams.toDate
)
preload(
hotel.id,
params.lang,
fromDate.format("YYYY-MM-DD"),
toDate.format("YYYY-MM-DD"),
adultsInRoom,
childrenInRoom
)
const hotelData = await getHotel({
hotelId: hotel.id,
isCardOnlyPayment: false,
@@ -104,18 +91,17 @@ export default async function SelectRatePage({
<HotelInfoCard hotelData={hotelData} />
</Suspense>
<Suspense key={suspenseKey} fallback={<RoomsContainerSkeleton />}>
<RoomsContainer
adultArray={adultsInRoom}
booking={booking}
childArray={childrenInRoom}
fromDate={arrivalDate}
hotelId={hotelId}
lang={params.lang}
toDate={departureDate}
/>
</Suspense>
<Suspense key={suspenseKey} fallback={null}>
<RoomsContainer
hotelData={hotelData}
adultArray={adultsInRoom}
booking={booking}
childArray={childrenInRoom}
fromDate={arrivalDate}
hotelId={hotelId}
toDate={departureDate}
/>
<Suspense key={`${suspenseKey}-tracking`} fallback={null}>
<TrackingSDK
pageData={pageTrackingData}
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 { Lang } from "@/constants/languages"
export function combineRoomAvailabilities(
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 {
GetHotelsByCSFilterInput,
GetRoomsAvailabilityInput,
GetSelectedRoomAvailabilityInput,
} from "@/server/routers/hotels/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()
})
export const getRoomsAvailability = cache(
async function getMemoizedRoomAvailability(input: GetRoomsAvailabilityInput) {
return serverClient().hotel.availability.rooms(input)
}
)
export const getSelectedRoomAvailability = cache(
function getMemoizedSelectedRoomAvailability(
input: GetSelectedRoomAvailabilityInput

View File

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

View File

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

View File

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

View File

@@ -35,16 +35,6 @@ export const hotelsByHotelIdAvailabilityFailCounter = meter.createCounter(
"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(
"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"
export interface RoomsContainerProps {
@@ -7,6 +7,6 @@ export interface RoomsContainerProps {
childArray?: Child[]
fromDate: Date
hotelId: number
lang: Lang
toDate: Date
hotelData: HotelData | null
}