"use client" import { zodResolver } from "@hookform/resolvers/zod" import { useSearchParams } from "next/navigation" import { use, useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { dt } from "@scandic-hotels/common/dt" import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition" import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position" import { debounce } from "@scandic-hotels/common/utils/debounce" import isValidJson from "@scandic-hotels/common/utils/isValidJson" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" 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, bookingWidgetSchema, } from "./BookingWidgetForm/schema" import Form from "./BookingWidgetForm" import MobileToggleButton from "./MobileToggleButton" import { BookingWidgetSkeleton } from "./Skeleton" import { bookingWidgetContainerVariants, formContainerVariants, } from "./variant" import styles from "./bookingWidget.module.css" import type { z } from "zod" import type { BookingWidgetSearchData, BookingWidgetType } from "." export type BookingWidgetSchema = z.output export type BookingCodeSchema = z.output export type BookingWidgetClientProps = { type?: BookingWidgetType data: BookingWidgetSearchData pageSettingsBookingCodePromise: Promise | null } export default function BookingWidgetClient({ type, data, pageSettingsBookingCodePromise, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) const bookingWidgetRef = useRef(null) const lang = useLang() const [originalOverflowY, setOriginalOverflowY] = useState( null ) const { bookingCodeEnabled } = useBookingFlowConfig() const storedBookingWidgetState = useBookingWidgetState() const shouldFetchAutoComplete = !!data.hotelId || !!data.city const { data: destinationsData, isPending } = trpc.autocomplete.destinations.useQuery( { lang, query: "", includeTypes: ["hotels", "cities"], selectedHotelId: data.hotelId ? data.hotelId.toString() : undefined, selectedCity: data.city, }, { enabled: shouldFetchAutoComplete } ) const shouldShowSkeleton = shouldFetchAutoComplete && isPending useStickyPosition({ ref: bookingWidgetRef, name: StickyElementNameEnum.BOOKING_WIDGET, }) 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(data.fromDate) let toDate = dt(data.toDate) const isDateParamValid = fromDate.isValid() && toDate.isValid() && fromDate.isSameOrAfter(now, "day") && toDate.isAfter(fromDate) if (!isDateParamValid) { fromDate = now toDate = now.add(1, "day") } const selectedLocation = destinationsData?.currentSelection.hotel ?? destinationsData?.currentSelection.city // if bookingCode is not provided in the search params, // we will fetch it from the page settings stored in Contentstack. const selectedBookingCode = data.bookingCode || (pageSettingsBookingCodePromise !== null ? use(pageSettingsBookingCodePromise) : "") const defaultRoomsData: BookingWidgetSchema["rooms"] = data.rooms?.map( (room) => ({ adults: room.adults, childrenInRoom: room.childrenInRoom || [], }) ) ?? [ { adults: 1, childrenInRoom: [], }, ] const hotelId = data.hotelId ? parseInt(data.hotelId) : undefined const methods = useForm({ defaultValues: { search: selectedLocation?.name ?? "", // Only used for displaying the selected location for mobile, not for actual form input selectedSearch: selectedLocation?.name ?? "", date: { fromDate: fromDate.format("YYYY-MM-DD"), toDate: toDate.format("YYYY-MM-DD"), }, bookingCode: { value: bookingCodeEnabled ? selectedBookingCode : "", remember: false, }, redemption: data.searchType === SEARCH_TYPE_REDEMPTION, rooms: defaultRoomsData, city: data.city || undefined, hotel: hotelId, }, shouldFocusError: false, mode: "onSubmit", resolver: zodResolver(bookingWidgetSchema), reValidateMode: "onSubmit", }) const searchParams = useSearchParams() const bookingCodeFromSearchParams = searchParams.get("bookingCode") || "" const [bookingCode, setBookingCode] = useState(bookingCodeFromSearchParams) if (bookingCode !== bookingCodeFromSearchParams) { methods.setValue("bookingCode", { value: bookingCodeFromSearchParams, }) setBookingCode(bookingCodeFromSearchParams) } useEffect(() => { if (!selectedLocation) return /* If `trpc.hotel.locations.get.useQuery` hasn't been fetched previously and is hence async we need to update the default values when data is available */ methods.setValue("search", selectedLocation.name) methods.setValue("selectedSearch", selectedLocation.name) }, [selectedLocation, methods]) function closeMobileSearch() { setIsOpen(false) const overflowY = originalOverflowY ?? "visible" document.body.style.overflowY = overflowY } function openMobileSearch() { setIsOpen(true) setOriginalOverflowY(document.body.style.overflowY) document.body.style.overflowY = "hidden" } useEffect(() => { const observer = new ResizeObserver( debounce(([entry]) => { if (entry.contentRect.width > 768) { setIsOpen(false) document.body.style.removeProperty("overflow-y") } }) ) observer.observe(document.body) return () => { observer.unobserve(document.body) } }, []) 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 if (initialBookingCode?.remember) { methods.setValue("bookingCode", initialBookingCode) } } }, [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 } const classNames = bookingWidgetContainerVariants({ type, }) const formContainerClassNames = formContainerVariants({ type, }) return (
) }