fix: improve handling of booking widget params from search params

now we are defensive in parsing the location
if parsing fails the not found is now displayed
This commit is contained in:
Michael Zetterberg
2025-02-25 13:29:39 +01:00
parent 15fa01cbb2
commit 43c25aea95
3 changed files with 66 additions and 55 deletions

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
@@ -30,6 +31,25 @@ import type {
} from "@/types/components/bookingWidget" } from "@/types/components/bookingWidget"
import type { Location } from "@/types/trpc/routers/hotel/locations" import type { Location } from "@/types/trpc/routers/hotel/locations"
function getLocationObj(locations: Location[], destination: string) {
try {
const location = locations.find((location) => {
if (location.type === "hotels") {
return location.operaId === destination
} else if (location.type === "cities") {
return location.name.toLowerCase() === destination.toLowerCase()
}
})
if (location) {
return location
}
} catch (_) {
// ignore any errors
}
return null
}
export default function BookingWidgetClient({ export default function BookingWidgetClient({
locations, locations,
type, type,
@@ -37,80 +57,62 @@ export default function BookingWidgetClient({
}: BookingWidgetClientProps) { }: BookingWidgetClientProps) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const bookingWidgetRef = useRef(null) const bookingWidgetRef = useRef(null)
useStickyPosition({ useStickyPosition({
ref: bookingWidgetRef, ref: bookingWidgetRef,
name: StickyElementNameEnum.BOOKING_WIDGET, name: StickyElementNameEnum.BOOKING_WIDGET,
}) })
const bookingWidgetSearchData = bookingWidgetSearchParams const params = convertSearchParamsToObj<BookingWidgetSearchData>(
? convertSearchParamsToObj<BookingWidgetSearchData>( bookingWidgetSearchParams
bookingWidgetSearchParams )
)
: undefined
const getLocationObj = (destination: string): Location | undefined => {
if (destination) {
const location: Location | undefined = locations.find((location) => {
return (
location.name.toLowerCase() === destination.toLowerCase() ||
//@ts-ignore (due to operaId not property error)
(location.operaId && location.operaId == destination)
)
})
return location
}
return undefined
}
const reqFromDate = bookingWidgetSearchData?.fromDate?.toString()
const reqToDate = bookingWidgetSearchData?.toDate?.toString()
const parsedFromDate = reqFromDate ? dt(reqFromDate) : undefined
const parsedToDate = reqToDate ? dt(reqToDate) : undefined
const now = dt() const now = dt()
// if fromDate or toDate is undefined, dt will return value that represents the same as 'now' above.
// this is fine as isDateParamValid will catch this and default the values accordingly.
let fromDate = dt(params.fromDate)
let toDate = dt(params.toDate)
const isDateParamValid = const isDateParamValid =
parsedFromDate && fromDate.isValid() &&
parsedToDate && toDate.isValid() &&
parsedFromDate.isSameOrAfter(now, "day") && fromDate.isSameOrAfter(now, "day") &&
parsedToDate.isAfter(parsedFromDate) toDate.isAfter(fromDate)
const selectedLocation = bookingWidgetSearchData if (!isDateParamValid) {
? getLocationObj( fromDate = now
(bookingWidgetSearchData.hotelId ?? toDate = now.add(1, "day")
bookingWidgetSearchData.city) as string }
)
: undefined
const selectedBookingCode = bookingWidgetSearchData let selectedLocation: Location | null = null
? bookingWidgetSearchData.bookingCode
: ""
const defaultRoomsData: BookingWidgetSchema["rooms"] = if (params.hotelId) {
bookingWidgetSearchData?.rooms?.map((room) => ({ selectedLocation = getLocationObj(locations, params.hotelId)
} else if (params.city) {
selectedLocation = getLocationObj(locations, params.city)
}
const selectedBookingCode = params.bookingCode ?? ""
const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map(
(room) => ({
adults: room.adults, adults: room.adults,
childrenInRoom: room.childrenInRoom ?? [], childrenInRoom: room.childrenInRoom ?? [],
})) ?? [ })
{ ) ?? [
adults: 1, {
childrenInRoom: [], adults: 1,
}, childrenInRoom: [],
] },
]
const methods = useForm<BookingWidgetSchema>({ const methods = useForm<BookingWidgetSchema>({
defaultValues: { defaultValues: {
search: selectedLocation?.name ?? "", search: selectedLocation?.name ?? "",
location: selectedLocation ? JSON.stringify(selectedLocation) : undefined, location: selectedLocation ? JSON.stringify(selectedLocation) : undefined,
date: { date: {
// UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 fromDate: fromDate.format("YYYY-MM-DD"),
// This is specifically to handle timezones falling in different dates. toDate: toDate.format("YYYY-MM-DD"),
fromDate: isDateParamValid
? parsedFromDate.format("YYYY-MM-DD")
: now.utc().format("YYYY-MM-DD"),
toDate: isDateParamValid
? parsedToDate.format("YYYY-MM-DD")
: now.utc().add(1, "day").format("YYYY-MM-DD"),
}, },
bookingCode: { bookingCode: {
value: selectedBookingCode, value: selectedBookingCode,

View File

@@ -317,12 +317,21 @@ export const locationsSchema = z.object({
}, },
}, },
type: location.type, type: location.type,
operaId: location.attributes.operaId ?? "",
} }
}) })
) )
.transform((data) => .transform((data) =>
data data
.filter((node) => !!node) .filter((node) => !!node)
.filter((node) => {
if (node.type === "hotels") {
if (!node.operaId) {
return false
}
}
return true
})
.sort((a, b) => { .sort((a, b) => {
if (a.type === b.type) { if (a.type === b.type) {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)

View File

@@ -13,7 +13,7 @@ export const locationHotelSchema = z.object({
.optional(), .optional(),
keyWords: z.array(z.string()).optional(), keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""), name: z.string().optional().default(""),
operaId: z.string().optional(), operaId: z.coerce.string().optional(),
}), }),
id: z.string().optional().default(""), id: z.string().optional().default(""),
relationships: z relationships: z