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

View File

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

View File

@@ -27,15 +27,13 @@ import {
type SetStorageData,
} from "@/types/components/form/bookingwidget"
import type { SearchHistoryItem, SearchProps } from "@/types/components/search"
import type { Location, Locations } from "@/types/trpc/routers/hotel/locations"
const name = "search"
import type { Location } from "@/types/trpc/routers/hotel/locations"
export default function Search({ locations, handlePressEnter }: SearchProps) {
const { register, setValue, unregister, getValues } =
useFormContext<BookingWidgetSchema>()
const intl = useIntl()
const value = useWatch({ name })
const value = useWatch<BookingWidgetSchema, "search">({ name: "search" })
const locationString = getValues("location")
const location =
locationString && isValidJson(decodeURIComponent(locationString))
@@ -75,7 +73,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
) {
const newValue = evt.currentTarget.value
setValue(name, newValue)
setValue("search", newValue)
dispatchInputValue(value)
}
@@ -97,7 +95,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
const stringified = JSON.stringify(selectedItem)
setValue("location", encodeURIComponent(stringified))
sessionStorage.setItem(sessionStorageKey, stringified)
setValue(name, selectedItem.name)
setValue("search", selectedItem.name)
const newHistoryItem: SearchHistoryItem = {
type: selectedItem.type,
id: selectedItem.id,
@@ -114,7 +112,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
const searchHistory = [newHistoryItem, ...oldHistoryItems]
localStorage.setItem(localStorageKey, JSON.stringify(searchHistory))
const enhancedSearchHistory: Locations = [
const enhancedSearchHistory: Location[] = [
...getEnhancedSearchHistory([newHistoryItem], locations),
...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.
<input type="hidden" {...register(stayType)} />
) : null}
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
<label
{...getLabelProps({ htmlFor: "search" })}
className={styles.label}
>
<Caption
type="bold"
color={isOpen ? "uiTextActive" : "red"}
@@ -226,7 +227,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
<label className={styles.searchInput}>
<Input
{...getInputProps({
id: name,
id: "search",
onFocus(evt) {
handleOnFocus(evt)
openMenu()
@@ -234,7 +235,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
placeholder: intl.formatMessage({
id: "Hotels & Destinations",
}),
...register(name, {
...register("search", {
onChange: handleOnChange,
}),
onKeyDown: (e) => {
@@ -285,8 +286,8 @@ export function SearchSkeleton() {
*/
function getEnhancedSearchHistory(
searchHistory: SearchHistoryItem[],
locations: Locations
): Locations {
locations: Location[]
): Location[] {
return searchHistory
.map((historyItem) =>
locations.find(
@@ -294,5 +295,5 @@ function getEnhancedSearchHistory(
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 {
formState: { errors },
} = 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")
@@ -63,7 +61,7 @@ export default function FormContent({
<div className={styles.rooms}>
<label>
<Caption color="red" type="bold" asChild>
<span>{roomsLabel}</span>
<span>{intl.formatMessage({ id: "Rooms & Guests" })}</span>
</Caption>
</label>
<GuestsRoomsPickerForm />
@@ -86,7 +84,7 @@ export default function FormContent({
<Voucher />
</div>
<div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}>
{bookingCodeError?.message?.indexOf("Multi-room") === 0 ? (
{errors.bookingCode?.value?.message?.indexOf("Multi-room") === 0 ? (
<RemoveExtraRooms
size="medium"
fullWidth

View File

@@ -15,7 +15,7 @@ import { GuestsRoom } from "./GuestsRoom"
import styles from "./guests-rooms-picker.module.css"
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

View File

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

View File

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

View File

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

View File

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