chore: Cleanup booking widget with types and other minor issues

This commit is contained in:
Christian Andolf
2025-04-02 10:06:28 +02:00
parent 3c810d67a2
commit 14f9b68365
9 changed files with 92 additions and 98 deletions

View File

@@ -97,7 +97,7 @@ export default function BookingWidgetClient({
const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map( const defaultRoomsData: BookingWidgetSchema["rooms"] = params.rooms?.map(
(room) => ({ (room) => ({
adults: room.adults, adults: room.adults,
childrenInRoom: room.childrenInRoom ?? [], childrenInRoom: room.childrenInRoom,
}) })
) ?? [ ) ?? [
{ {
@@ -149,21 +149,18 @@ export default function BookingWidgetClient({
} }
useEffect(() => { useEffect(() => {
const debouncedResizeHandler = debounce(function ([ const observer = new ResizeObserver(
entry, debounce(([entry]) => {
]: ResizeObserverEntry[]) { if (entry.contentRect.width > 1366) {
if (entry.contentRect.width > 1366) { closeMobileSearch()
closeMobileSearch() }
} })
}) )
const observer = new ResizeObserver(debouncedResizeHandler)
observer.observe(document.body) observer.observe(document.body)
return () => { return () => {
if (observer) { observer.unobserve(document.body)
observer.unobserve(document.body)
}
} }
}, []) }, [])
@@ -176,13 +173,15 @@ export default function BookingWidgetClient({
? JSON.parse(sessionStorageSearchData) ? JSON.parse(sessionStorageSearchData)
: undefined : undefined
initialSelectedLocation?.name && if (initialSelectedLocation?.name) {
methods.setValue("search", initialSelectedLocation.name) methods.setValue("search", initialSelectedLocation.name)
sessionStorageSearchData && }
if (sessionStorageSearchData) {
methods.setValue( methods.setValue(
"location", "location",
encodeURIComponent(sessionStorageSearchData) encodeURIComponent(sessionStorageSearchData)
) )
}
} }
}, [methods, selectedLocation]) }, [methods, selectedLocation])

View File

@@ -27,21 +27,23 @@ export default function MobileToggleButton({
}: BookingWidgetToggleButtonProps) { }: BookingWidgetToggleButtonProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang() const lang = useLang()
const d = useWatch({ name: "date" }) const date = useWatch<BookingWidgetSchema, "date">({ name: "date" })
const location = useWatch({ name: "location" }) const location = useWatch<BookingWidgetSchema, "location">({
const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" }) name: "location",
})
const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const parsedLocation: Location | null = const parsedLocation: Location | null =
location && isValidJson(location) location && isValidJson(location)
? JSON.parse(decodeURIComponent(location)) ? JSON.parse(decodeURIComponent(location))
: null : null
const selectedFromDate = dt(d.fromDate).locale(lang).format("D MMM") const selectedFromDate = dt(date.fromDate).locale(lang).format("D MMM")
const selectedToDate = dt(d.toDate).locale(lang).format("D MMM") const selectedToDate = dt(date.toDate).locale(lang).format("D MMM")
const locationAndDateIsSet = parsedLocation && d const locationAndDateIsSet = parsedLocation && date
const totalNights = dt(d.toDate).diff(dt(d.fromDate), "days") const totalNights = dt(date.toDate).diff(dt(date.fromDate), "days")
const totalRooms = rooms.length const totalRooms = rooms.length
const totalAdults = rooms.reduce((acc, room) => { const totalAdults = rooms.reduce((acc, room) => {
if (room.adults) { if (room.adults) {

View File

@@ -27,15 +27,13 @@ import {
type SetStorageData, type SetStorageData,
} from "@/types/components/form/bookingwidget" } from "@/types/components/form/bookingwidget"
import type { SearchHistoryItem, SearchProps } from "@/types/components/search" import type { SearchHistoryItem, SearchProps } from "@/types/components/search"
import type { Location, Locations } from "@/types/trpc/routers/hotel/locations" import type { Location } from "@/types/trpc/routers/hotel/locations"
const name = "search"
export default function Search({ locations, handlePressEnter }: SearchProps) { export default function Search({ locations, handlePressEnter }: SearchProps) {
const { register, setValue, unregister, getValues } = const { register, setValue, unregister, getValues } =
useFormContext<BookingWidgetSchema>() useFormContext<BookingWidgetSchema>()
const intl = useIntl() const intl = useIntl()
const value = useWatch({ name }) const value = useWatch<BookingWidgetSchema, "search">({ name: "search" })
const locationString = getValues("location") const locationString = getValues("location")
const location = const location =
locationString && isValidJson(decodeURIComponent(locationString)) locationString && isValidJson(decodeURIComponent(locationString))
@@ -75,7 +73,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement> evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
) { ) {
const newValue = evt.currentTarget.value const newValue = evt.currentTarget.value
setValue(name, newValue) setValue("search", newValue)
dispatchInputValue(value) dispatchInputValue(value)
} }
@@ -97,7 +95,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
const stringified = JSON.stringify(selectedItem) const stringified = JSON.stringify(selectedItem)
setValue("location", encodeURIComponent(stringified)) setValue("location", encodeURIComponent(stringified))
sessionStorage.setItem(sessionStorageKey, stringified) sessionStorage.setItem(sessionStorageKey, stringified)
setValue(name, selectedItem.name) setValue("search", selectedItem.name)
const newHistoryItem: SearchHistoryItem = { const newHistoryItem: SearchHistoryItem = {
type: selectedItem.type, type: selectedItem.type,
id: selectedItem.id, id: selectedItem.id,
@@ -114,7 +112,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
const searchHistory = [newHistoryItem, ...oldHistoryItems] const searchHistory = [newHistoryItem, ...oldHistoryItems]
localStorage.setItem(localStorageKey, JSON.stringify(searchHistory)) localStorage.setItem(localStorageKey, JSON.stringify(searchHistory))
const enhancedSearchHistory: Locations = [ const enhancedSearchHistory: Location[] = [
...getEnhancedSearchHistory([newHistoryItem], locations), ...getEnhancedSearchHistory([newHistoryItem], locations),
...oldSearchHistoryWithoutTheNew, ...oldSearchHistoryWithoutTheNew,
] ]
@@ -213,7 +211,10 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
// Adding hidden input to define hotel or city based on destination selection for basic form submit. // Adding hidden input to define hotel or city based on destination selection for basic form submit.
<input type="hidden" {...register(stayType)} /> <input type="hidden" {...register(stayType)} />
) : null} ) : null}
<label {...getLabelProps({ htmlFor: name })} className={styles.label}> <label
{...getLabelProps({ htmlFor: "search" })}
className={styles.label}
>
<Caption <Caption
type="bold" type="bold"
color={isOpen ? "uiTextActive" : "red"} color={isOpen ? "uiTextActive" : "red"}
@@ -226,7 +227,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
<label className={styles.searchInput}> <label className={styles.searchInput}>
<Input <Input
{...getInputProps({ {...getInputProps({
id: name, id: "search",
onFocus(evt) { onFocus(evt) {
handleOnFocus(evt) handleOnFocus(evt)
openMenu() openMenu()
@@ -234,7 +235,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
placeholder: intl.formatMessage({ placeholder: intl.formatMessage({
id: "Hotels & Destinations", id: "Hotels & Destinations",
}), }),
...register(name, { ...register("search", {
onChange: handleOnChange, onChange: handleOnChange,
}), }),
onKeyDown: (e) => { onKeyDown: (e) => {
@@ -285,8 +286,8 @@ export function SearchSkeleton() {
*/ */
function getEnhancedSearchHistory( function getEnhancedSearchHistory(
searchHistory: SearchHistoryItem[], searchHistory: SearchHistoryItem[],
locations: Locations locations: Location[]
): Locations { ): Location[] {
return searchHistory return searchHistory
.map((historyItem) => .map((historyItem) =>
locations.find( locations.find(
@@ -294,5 +295,5 @@ function getEnhancedSearchHistory(
location.type === historyItem.type && location.id === historyItem.id location.type === historyItem.type && location.id === historyItem.id
) )
) )
.filter((r) => !!r) as Locations .filter((r): r is Location => !!r)
} }

View File

@@ -31,10 +31,8 @@ export default function FormContent({
const { const {
formState: { errors }, formState: { errors },
} = useFormContext<BookingWidgetSchema>() } = useFormContext<BookingWidgetSchema>()
const bookingCodeError = errors["bookingCode"]?.value
const selectedDate = useWatch({ name: "date" })
const roomsLabel = intl.formatMessage({ id: "Rooms & Guests" }) const selectedDate = useWatch({ name: "date" })
const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days")
@@ -63,7 +61,7 @@ export default function FormContent({
<div className={styles.rooms}> <div className={styles.rooms}>
<label> <label>
<Caption color="red" type="bold" asChild> <Caption color="red" type="bold" asChild>
<span>{roomsLabel}</span> <span>{intl.formatMessage({ id: "Rooms & Guests" })}</span>
</Caption> </Caption>
</label> </label>
<GuestsRoomsPickerForm /> <GuestsRoomsPickerForm />
@@ -86,7 +84,7 @@ export default function FormContent({
<Voucher /> <Voucher />
</div> </div>
<div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}> <div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}>
{bookingCodeError?.message?.indexOf("Multi-room") === 0 ? ( {errors.bookingCode?.value?.message?.indexOf("Multi-room") === 0 ? (
<RemoveExtraRooms <RemoveExtraRooms
size="medium" size="medium"
fullWidth fullWidth

View File

@@ -15,7 +15,7 @@ import { GuestsRoom } from "./GuestsRoom"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget" import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" import type { GuestsRoom as TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
const MAX_ROOMS = 4 const MAX_ROOMS = 4

View File

@@ -12,14 +12,14 @@ import ChildSelector from "../ChildSelector"
import styles from "../guests-rooms-picker.module.css" import styles from "../guests-rooms-picker.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export function GuestsRoom({ export function GuestsRoom({
room, room,
index, index,
onRemove, onRemove,
}: { }: {
room: TGuestsRoom room: GuestsRoom
index: number index: number
onRemove: (index: number) => void onRemove: (index: number) => void
}) { }) {
@@ -54,22 +54,18 @@ export function GuestsRoom({
childrenInAdultsBed={childrenInAdultsBed} childrenInAdultsBed={childrenInAdultsBed}
/> />
{index !== 0 && ( {index !== 0 && (
<div className={styles.roomActions}> <Button
<Button intent="text"
intent="text" variant="icon"
variant="icon" wrapping
wrapping theme="secondaryLight"
theme="secondaryLight" onPress={() => onRemove(index)}
onPress={() => onRemove(index)} size="small"
size="small" className={styles.roomActionsButton}
className={styles.roomActionsButton} >
> <MaterialIcon icon="delete" color="CurrentColor" />
<MaterialIcon icon="delete" color="CurrentColor" /> {intl.formatMessage({ id: "Remove room" })}
<span className={styles.roomActionsLabel}> </Button>
{intl.formatMessage({ id: "Remove room" })}
</span>
</Button>
</div>
)} )}
</section> </section>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useId, useState } from "react"
import { import {
Button, Button,
Dialog, Dialog,
@@ -8,7 +8,7 @@ import {
Modal, Modal,
Popover, Popover,
} from "react-aria-components" } from "react-aria-components"
import { useFormContext } from "react-hook-form" import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts" import { useMediaQuery } from "usehooks-ts"
@@ -18,23 +18,23 @@ import PickerForm from "./Form"
import styles from "./guests-rooms-picker.module.css" import styles from "./guests-rooms-picker.module.css"
import type { TGuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker" import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import type { GuestsRoom } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function GuestsRoomsPickerForm() { export default function GuestsRoomsPickerForm() {
const { watch, trigger } = useFormContext() const { trigger } = useFormContext<BookingWidgetSchema>()
const rooms = watch("rooms") as TGuestsRoom[] const rooms = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const popoverId = useId()
const checkIsDesktop = useMediaQuery("(min-width: 1367px)") const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
const [isDesktop, setIsDesktop] = useState(true) const [isDesktop, setIsDesktop] = useState(true)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [containerHeight, setContainerHeight] = useState(0) const [containerHeight, setContainerHeight] = useState(0)
const childCount = const childCount = rooms[0]?.childrenInRoom.length ?? 0 // ToDo Update for multiroom later
rooms[0] && rooms[0].childrenInRoom ? rooms[0].childrenInRoom.length : 0 // ToDo Update for multiroom later
const htmlElement =
typeof window !== "undefined" ? document.querySelector("body") : null
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed". //isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
async function setOverflowClip(isOpen: boolean) { async function setOverflowClip(isOpen: boolean) {
const htmlElement = document.body
if (htmlElement) { if (htmlElement) {
if (isOpen) { if (isOpen) {
htmlElement.style.overflow = "visible" htmlElement.style.overflow = "visible"
@@ -57,34 +57,32 @@ export default function GuestsRoomsPickerForm() {
}, [checkIsDesktop]) }, [checkIsDesktop])
const updateHeight = useCallback(() => { const updateHeight = useCallback(() => {
if (typeof window !== undefined) { // Get available space for picker to show without going beyond screen
// Get available space for picker to show without going beyond screen const bookingWidget = document.getElementById("booking-widget")
let maxHeight = let maxHeight =
window.innerHeight - window.innerHeight -
(document.querySelector("#booking-widget")?.getBoundingClientRect() (bookingWidget?.getBoundingClientRect().bottom ?? 0) -
.bottom ?? 0) - 50
50 const innerContainerHeight = document
const innerContainerHeight = document .getElementsByClassName(popoverId)[0]
.querySelector(".guests_picker_popover") ?.getBoundingClientRect().height
?.getBoundingClientRect().height if (
if ( maxHeight != containerHeight &&
maxHeight != containerHeight && innerContainerHeight &&
innerContainerHeight && maxHeight <= innerContainerHeight
maxHeight <= innerContainerHeight ) {
) { setContainerHeight(maxHeight)
setContainerHeight(maxHeight) } else if (
} else if ( containerHeight &&
containerHeight && innerContainerHeight &&
innerContainerHeight && maxHeight > innerContainerHeight
maxHeight > innerContainerHeight ) {
) { setContainerHeight(0)
setContainerHeight(0)
}
} }
}, [containerHeight]) }, [containerHeight, popoverId])
useEffect(() => { useEffect(() => {
if (typeof window !== undefined && isDesktop && rooms.length > 0) { if (isDesktop && rooms.length > 0) {
updateHeight() updateHeight()
} }
}, [childCount, isDesktop, updateHeight, rooms]) }, [childCount, isDesktop, updateHeight, rooms])
@@ -99,10 +97,10 @@ export default function GuestsRoomsPickerForm() {
}} }}
/> />
<Popover <Popover
className="guests_picker_popover" className={popoverId}
placement="bottom start" placement="bottom start"
offset={36} offset={36}
style={containerHeight ? { overflow: "auto" } : {}} style={containerHeight ? { overflow: "auto" } : undefined}
> >
<Dialog className={styles.pickerContainerDesktop}> <Dialog className={styles.pickerContainerDesktop}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />} {({ close }) => <PickerForm rooms={rooms} onClose={close} />}
@@ -132,7 +130,7 @@ function Trigger({
className, className,
triggerFn, triggerFn,
}: { }: {
rooms: TGuestsRoom[] rooms: GuestsRoom[]
className: string className: string
triggerFn?: () => void triggerFn?: () => void
}) { }) {

View File

@@ -5,7 +5,7 @@ export type ChildBed = {
value: number value: number
} }
export type TGuestsRoom = Required<Pick<Room, "adults" | "childrenInRoom">> export type GuestsRoom = Required<Pick<Room, "adults" | "childrenInRoom">>
export type GuestsRoomPickerProps = { export type GuestsRoomPickerProps = {
index: number index: number

View File

@@ -7,7 +7,7 @@ import type {
bookingWidgetSchema, bookingWidgetSchema,
} from "@/components/Forms/BookingWidget/schema" } from "@/components/Forms/BookingWidget/schema"
import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants" import type { bookingWidgetVariants } from "@/components/Forms/BookingWidget/variants"
import type { TGuestsRoom } from "./guestsRoomsPicker" import type { GuestsRoom } from "./guestsRoomsPicker"
export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema> export type BookingWidgetSchema = z.output<typeof bookingWidgetSchema>
export type BookingCodeSchema = z.output<typeof bookingCodeSchema> export type BookingCodeSchema = z.output<typeof bookingCodeSchema>
@@ -17,7 +17,7 @@ export type BookingWidgetSearchData = {
hotel?: string hotel?: string
fromDate?: string fromDate?: string
toDate?: string toDate?: string
rooms?: TGuestsRoom[] rooms?: GuestsRoom[]
bookingCode?: string bookingCode?: string
searchType?: "redemption" searchType?: "redemption"
} }