Merged in feat/BOOK-377-keep-booking-widget-state (pull request #2894)

feat(BOOK-377): keep dates and rooms & guest in session storage

* feat(BOOK-377): keep dates and rooms & guest in session storage

* feat(BOOK-377): extract to hook and reuse


Approved-by: Erik Tiekstra
This commit is contained in:
Bianca Widstam
2025-10-06 09:33:09 +00:00
parent cbeba7eb08
commit f726b4f7e1
9 changed files with 137 additions and 16 deletions

View File

@@ -1,7 +1,8 @@
"use client"
import { useBookingWidgetState } from "@scandic-hotels/booking-flow/hooks/useBookingWidgetState"
import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url"
import { selectRateWithParams } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { dt } from "@scandic-hotels/common/dt"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import useLang from "@/hooks/useLang"
@@ -15,9 +16,15 @@ export default function PricesAndAvailabilityButton({
label,
}: PricesAndAvailabilityProps) {
const lang = useLang()
const fromdate = dt().format("YYYY-MM-DD")
const todate = dt().add(1, "day").format("YYYY-MM-DD")
const selectRateURL = selectRateWithParams(lang, hotelId, fromdate, todate)
const { fromDate, toDate, rooms } = useBookingWidgetState()
const params = serializeBookingSearchParams({
hotelId,
fromDate,
toDate,
rooms,
})
const selectRateURL = selectRateWithParams(lang, params)
const { name, roomTypes } = room

View File

@@ -13,6 +13,7 @@ import {
import { trackBookingSearchClick } from "@scandic-hotels/tracking/booking"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { setBookingWidgetState } from "../../../hooks/useBookingWidgetState"
import useLang from "../../../hooks/useLang"
import {
BookingCodeFilterEnum,
@@ -68,7 +69,11 @@ export default function Form({ type, onClose }: BookingWidgetFormProps) {
// Followed current url structure to keep searchtype=redemption param incase of reward night
...(data.redemption ? { searchType: SEARCH_TYPE_REDEMPTION } : {}),
})
setBookingWidgetState({
fromDate: data.date.fromDate,
toDate: data.date.toDate,
rooms: data.rooms,
})
onClose()
startTransition(() => {
router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`)

View File

@@ -15,6 +15,7 @@ import { trpc } from "@scandic-hotels/trpc/client"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { useBookingFlowConfig } from "../../bookingFlowConfig/bookingFlowConfigContext"
import { useBookingWidgetState } from "../../hooks/useBookingWidgetState"
import useLang from "../../hooks/useLang"
import {
type bookingCodeSchema,
@@ -54,6 +55,7 @@ export default function BookingWidgetClient({
null
)
const { bookingCodeEnabled } = useBookingFlowConfig()
const storedBookingWidgetState = useBookingWidgetState()
const shouldFetchAutoComplete = !!data.hotelId || !!data.city
@@ -207,6 +209,24 @@ export default function BookingWidgetClient({
}
}, [methods, selectedBookingCode])
useEffect(() => {
if (
!data.fromDate &&
!data.toDate &&
!data.rooms &&
storedBookingWidgetState
) {
methods.reset({
...methods.getValues(),
date: {
fromDate: storedBookingWidgetState.fromDate,
toDate: storedBookingWidgetState.toDate,
},
rooms: storedBookingWidgetState.rooms,
})
}
}, [data, methods, storedBookingWidgetState])
if (shouldShowSkeleton) {
return <BookingWidgetSkeleton type={type} />
}

View File

@@ -0,0 +1,92 @@
import { useEffect, useState } from "react"
import { z } from "zod"
import { BOOKING_WIDGET_STATE } from "@scandic-hotels/common/constants/bookingWidget"
import { dt } from "@scandic-hotels/common/dt"
import { adultsSchema, childAgeSchema, childBedSchema } from "../utils/url"
const DEFAULT_ROOMS = [{ adults: 1, childrenInRoom: [] }]
const stateSchema = z.object({
fromDate: z.string(),
toDate: z.string(),
rooms: z.array(
z.object({
adults: adultsSchema,
childrenInRoom: z
.array(
z.object({
bed: childBedSchema,
age: childAgeSchema,
})
)
.optional(),
})
),
})
type BookingWidgetState = z.infer<typeof stateSchema>
export function useBookingWidgetState() {
const [bookingState, setBookingState] = useState<BookingWidgetState>({
fromDate: dt().format("YYYY-MM-DD"),
toDate: dt().add(1, "day").format("YYYY-MM-DD"),
rooms: DEFAULT_ROOMS,
})
useEffect(() => {
const now = dt()
const stored = getBookingWidgetState()
if (!stored) {
return
}
const storedFrom = dt(stored.fromDate)
const storedTo = dt(stored.toDate)
const isDateParamValid =
storedFrom.isValid() &&
storedTo.isValid() &&
storedFrom.isSameOrAfter(now, "day") &&
storedTo.isAfter(storedFrom)
if (isDateParamValid && stored.rooms?.length) {
setBookingState({
fromDate: storedFrom.format("YYYY-MM-DD"),
toDate: storedTo.format("YYYY-MM-DD"),
rooms: stored.rooms,
})
}
}, [])
return bookingState
}
function getBookingWidgetState(): BookingWidgetState | null {
if (typeof window === "undefined") {
return null
}
try {
const savedBookingWidgetState = sessionStorage.getItem(BOOKING_WIDGET_STATE)
if (savedBookingWidgetState) {
const stored = JSON.parse(savedBookingWidgetState)
return stateSchema.parse(stored)
}
} catch {
try {
sessionStorage.removeItem(BOOKING_WIDGET_STATE)
} catch {}
}
return null
}
export function setBookingWidgetState(state: BookingWidgetState): void {
if (typeof window === "undefined") {
return
}
try {
sessionStorage.setItem(
BOOKING_WIDGET_STATE,
JSON.stringify(stateSchema.parse(state))
)
} catch {}
}

View File

@@ -39,9 +39,9 @@ const typeHints = {
filters: "COMMA_SEPARATED_ARRAY",
packages: "COMMA_SEPARATED_ARRAY",
} as const
const adultsSchema = z.coerce.number().min(1).max(6).catch(0)
const childAgeSchema = z.coerce.number().catch(-1)
const childBedSchema = z.coerce.number().catch(-1)
export const adultsSchema = z.coerce.number().min(1).max(6).catch(0)
export const childAgeSchema = z.coerce.number().catch(-1)
export const childBedSchema = z.coerce.number().catch(-1)
const searchTypeSchema = z.enum(bookingSearchTypes).optional().catch(undefined)
export function parseBookingWidgetSearchParams(

View File

@@ -28,6 +28,7 @@
"./components/SidePeekAccordions/ParkingAccordionItem": "./lib/components/SidePeekAccordions/ParkingAccordionItem.tsx",
"./global.d.ts": "./global.d.ts",
"./hooks/useHandleBookingStatus": "./lib/hooks/useHandleBookingStatus.ts",
"./hooks/useBookingWidgetState": "./lib/hooks/useBookingWidgetState.ts",
"./pages/*": "./lib/pages/*.tsx",
"./stores/enter-details/types": "./lib/stores/enter-details/types.ts",
"./stores/hotels-map": "./lib/stores/hotels-map.ts",

View File

@@ -0,0 +1 @@
export const BOOKING_WIDGET_STATE = "bookingWidgetState"

View File

@@ -27,14 +27,8 @@ export function selectHotel(lang: Lang) {
export function selectHotelMap(lang: Lang) {
return `${hotelreservation(lang)}/select-hotel/map`
}
export function selectRateWithParams(
lang: Lang,
hotelId: string,
fromdate: string,
todate: string
) {
return `${hotelreservation(lang)}/select-rate?room%5B0%5D.adults=1&fromdate=${fromdate}&todate=${todate}&hotel=${hotelId}`
export function selectRateWithParams(lang: Lang, params: URLSearchParams) {
return `${hotelreservation(lang)}/select-rate?${params.toString()}`
}
export function alternativeHotels(lang: Lang) {

View File

@@ -14,6 +14,7 @@
"./polyfills": "./polyfills/index.ts",
"./constants/alert": "./constants/alert.ts",
"./constants/booking": "./constants/booking.ts",
"./constants/bookingWidget": "./constants/bookingWidget.ts",
"./constants/currency": "./constants/currency.ts",
"./constants/dateFormats": "./constants/dateFormats.ts",
"./constants/facilities": "./constants/facilities.ts",