"use client" import { zodResolver } from "@hookform/resolvers/zod" import { use, useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { REDEMPTION } from "@/constants/booking" import { dt } from "@/lib/dt" import { trpc } from "@/lib/trpc/client" import { StickyElementNameEnum } from "@/stores/sticky-position" import Form, { BookingWidgetFormSkeleton, } from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import useLang from "@/hooks/useLang" import useStickyPosition from "@/hooks/useStickyPosition" import { debounce } from "@/utils/debounce" import isValidJson from "@/utils/isValidJson" import { convertSearchParamsToObj } from "@/utils/url" import MobileToggleButton, { MobileToggleButtonSkeleton, } from "./MobileToggleButton" import styles from "./bookingWidget.module.css" import type { BookingCodeSchema, BookingWidgetClientProps, BookingWidgetSchema, BookingWidgetSearchData, } from "@/types/components/bookingWidget" import type { Location } from "@/types/trpc/routers/hotel/locations" export default function BookingWidgetClient({ type, bookingWidgetSearchParams, pageSettingsBookingCodePromise, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) const bookingWidgetRef = useRef(null) const lang = useLang() const { data: locations, isLoading, isSuccess, } = trpc.hotel.locations.get.useQuery({ lang, }) useStickyPosition({ ref: bookingWidgetRef, name: StickyElementNameEnum.BOOKING_WIDGET, }) const params = convertSearchParamsToObj( bookingWidgetSearchParams ) 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 = fromDate.isValid() && toDate.isValid() && fromDate.isSameOrAfter(now, "day") && toDate.isAfter(fromDate) if (!isDateParamValid) { fromDate = now toDate = now.add(1, "day") } let selectedLocation: Location | null = null if (params.hotelId) { selectedLocation = getLocationObj(locations ?? [], params.hotelId) } else if (params.city) { selectedLocation = getLocationObj(locations ?? [], params.city) } // if bookingCode is not provided in the search params, // we will fetch it from the page settings stored in Contentstack. const selectedBookingCode = params.bookingCode || (pageSettingsBookingCodePromise !== null ? use(pageSettingsBookingCodePromise) : "") const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map( (room) => ({ adults: room.adults, childrenInRoom: room.childrenInRoom ?? [], }) ) ?? [ { adults: 1, childrenInRoom: [], }, ] const methods = useForm({ defaultValues: { search: selectedLocation?.name ?? "", location: selectedLocation ? JSON.stringify(selectedLocation) : undefined, date: { fromDate: fromDate.format("YYYY-MM-DD"), toDate: toDate.format("YYYY-MM-DD"), }, bookingCode: { value: selectedBookingCode, remember: false, }, redemption: params?.searchType === REDEMPTION, rooms: defaultRoomsData, }, shouldFocusError: false, mode: "onSubmit", resolver: zodResolver(bookingWidgetSchema), reValidateMode: "onSubmit", }) function closeMobileSearch() { setIsOpen(false) document.body.style.overflowY = "visible" } function openMobileSearch() { setIsOpen(true) document.body.style.overflowY = "hidden" } useEffect(() => { const debouncedResizeHandler = debounce(function ([ entry, ]: ResizeObserverEntry[]) { if (entry.contentRect.width > 1366) { closeMobileSearch() } }) const observer = new ResizeObserver(debouncedResizeHandler) observer.observe(document.body) return () => { if (observer) { observer.unobserve(document.body) } } }, []) useEffect(() => { if (typeof window !== "undefined" && !selectedLocation) { const sessionStorageSearchData = sessionStorage.getItem("searchData") const initialSelectedLocation: Location | undefined = sessionStorageSearchData && isValidJson(sessionStorageSearchData) ? JSON.parse(sessionStorageSearchData) : undefined initialSelectedLocation?.name && methods.setValue("search", initialSelectedLocation.name) sessionStorageSearchData && methods.setValue( "location", encodeURIComponent(sessionStorageSearchData) ) } }, [methods, selectedLocation]) useEffect(() => { if (!window?.sessionStorage || !window?.localStorage) return if (!selectedBookingCode) { const storedBookingCode = localStorage.getItem("bookingCode") const initialBookingCode: BookingCodeSchema | undefined = storedBookingCode && isValidJson(storedBookingCode) ? JSON.parse(storedBookingCode) : undefined initialBookingCode?.remember && methods.setValue("bookingCode", initialBookingCode) } }, [methods, selectedBookingCode]) if (isLoading) { return } if (!isSuccess || !locations) { // TODO: handle error cases return null } return (
) } export function BookingWidgetSkeleton() { return ( <>
) } 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 }