Files
web/components/HotelReservation/SelectRate/Rooms/index.tsx
Tobias Johansson b394d54c3f Merged in feat/enter-details-multiroom (pull request #1280)
feat(SW-1259): Enter details multiroom

* refactor: remove per-step URLs

* WIP: map multiroom data

* fix: lint errors in details page

* fix: made useEnterDetailsStore tests pass

* fix: WIP refactor enter details store

* fix: WIP enter details store update

* fix: added room index to select correct room

* fix: added logic for navigating between steps and rooms

* fix: update summary to work with store changes

* fix: added room and total price calculation

* fix: removed unused code and added test for breakfast included

* refactor: move store selectors into helpers

* refactor: session storage state for multiroom booking

* feat: update enter details accordion navigation

* fix: added room index to each form component so they select correct room

* fix: added unique id to input to handle case when multiple inputs have same name

* fix: update payment step with store changes

* fix: rebase issues

* fix: now you should only be able to go to a step if previous room is completed

* refactor: cleanup

* fix: if no availability just skip that room for now

* fix: add select-rate Summary and adjust typings


Approved-by: Arvid Norlin
2025-02-11 14:24:24 +00:00

288 lines
8.7 KiB
TypeScript

"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useEffect, useMemo } from "react"
import { useIntl } from "react-intl"
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { trackLowestRoomPrice } from "@/utils/tracking"
import { convertObjToSearchParams, convertSearchParamsToObj } from "@/utils/url"
import RateSummary from "../RateSummary"
import { RoomSelectionPanel } from "../RoomSelectionPanel"
import SelectedRoomPanel from "../SelectedRoomPanel"
import { roomSelectionPanelVariants } from "./variants"
import styles from "./rooms.module.css"
import {
type DefaultFilterOptions,
RoomPackageCodeEnum,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
export default function Rooms({
availablePackages,
hotelType,
isUserLoggedIn,
roomsAvailability,
roomCategories = [],
vat,
}: SelectRateProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const intl = useIntl()
const hotelId = searchParams.get("hotel")
const arrivalDate = searchParams.get("fromDate")
const departureDate = searchParams.get("toDate")
const {
selectedRates,
rateSummary,
calculateRateSummary,
initializeRates,
setGuestsInRooms,
} = useRateSelectionStore()
const {
selectedPackagesByRoom,
visibleRooms,
setVisibleRooms,
setRoomsAvailability,
getFilteredRooms,
} = useRoomFilteringStore()
const bookingWidgetSearchData = useMemo(
() =>
convertSearchParamsToObj<SelectRateSearchParams>(
Object.fromEntries(searchParams)
),
[searchParams]
)
useEffect(() => {
bookingWidgetSearchData.rooms.forEach((room, index) => {
setGuestsInRooms(index, room.adults, room.childrenInRoom)
})
}, [bookingWidgetSearchData.rooms, setGuestsInRooms])
const isMultipleRooms = bookingWidgetSearchData.rooms.length > 1
useEffect(() => {
initializeRates(bookingWidgetSearchData.rooms.length)
}, [initializeRates, bookingWidgetSearchData.rooms.length])
const defaultPackages: DefaultFilterOptions[] = useMemo(
() => [
{
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
description: intl.formatMessage({ id: "Accessible room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.ALLERGY_ROOM,
description: intl.formatMessage({ id: "Allergy-friendly room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
)?.itemCode,
},
{
code: RoomPackageCodeEnum.PET_ROOM,
description: intl.formatMessage({ id: "Pet room" }),
itemCode: availablePackages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
)?.itemCode,
},
],
[availablePackages, intl]
)
useEffect(() => {
setRoomsAvailability(roomsAvailability)
setVisibleRooms()
}, [roomsAvailability, setRoomsAvailability, setVisibleRooms])
useEffect(() => {
if (
selectedRates.length > 0 &&
selectedRates.some((rate) => rate !== undefined)
) {
calculateRateSummary({
getFilteredRooms,
availablePackages,
roomCategories,
selectedPackagesByRoom,
})
}
}, [
selectedRates,
getFilteredRooms,
availablePackages,
roomCategories,
selectedPackagesByRoom,
calculateRateSummary,
])
useEffect(() => {
if (!rateSummary?.some((rate) => rate === null)) return
const hasAnySelection = selectedRates.some((rate) => rate !== undefined)
if (!hasAnySelection) return
}, [rateSummary, selectedRates])
useEffect(() => {
const pricesWithCurrencies = visibleRooms.flatMap((room) =>
room.products.map((product) => ({
price: product.productType.public.localPrice.pricePerNight,
currency: product.productType.public.localPrice.currency,
}))
)
const lowestPrice = pricesWithCurrencies.reduce(
(minPrice, { price }) => Math.min(minPrice, price),
Infinity
)
const currency = pricesWithCurrencies[0]?.currency
trackLowestRoomPrice({
hotelId,
arrivalDate,
departureDate,
lowestPrice: lowestPrice,
currency: currency,
})
}, [arrivalDate, departureDate, hotelId, visibleRooms])
const queryParams = useMemo(() => {
const rooms = rateSummary.map((rate, index) => ({
roomTypeCode: rate?.roomTypeCode,
rateCode: rate?.public.rateCode,
counterRateCode: rate?.member?.rateCode,
packages: selectedPackagesByRoom[index] || [],
}))
const newSearchParams = convertObjToSearchParams({ rooms }, searchParams)
return newSearchParams
}, [searchParams, rateSummary, selectedPackagesByRoom])
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
window.history.pushState(null, "", `${pathname}?${queryParams.toString()}`)
router.push(`details?${queryParams}`)
}
useEffect(() => {
requestAnimationFrame(() => {
const SCROLL_OFFSET = 100
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
const index = selectedRates.findIndex((rate) => rate === undefined)
const targetIndex = index === -1 ? selectedRates.length - 1 : index - 1
const selectedRoom = roomElements[targetIndex]
if (selectedRoom) {
const elementPosition = selectedRoom.getBoundingClientRect().top
const offsetPosition = elementPosition + window.scrollY - SCROLL_OFFSET
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
})
}
})
}, [selectedRates])
return (
<div className={styles.content}>
{isMultipleRooms ? (
bookingWidgetSearchData.rooms.map((room, index) => {
const classNames = roomSelectionPanelVariants({
active:
(index === 0 || selectedRates[index - 1] !== undefined) &&
selectedRates[index] === undefined,
selected: selectedRates[index] !== undefined,
})
return (
<div key={index} className={styles.roomContainer}>
{selectedRates[index] === undefined && (
<Subtitle>
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
,{" "}
{intl.formatMessage(
{
id: room.childrenInRoom?.length
? "{adults} adults, {children} children"
: "{adults} adults",
},
{
adults: room.adults,
children: room.childrenInRoom?.length,
}
)}
</Subtitle>
)}
<div className={classNames}>
<div className={styles.roomPanel}>
<SelectedRoomPanel
roomIndex={index}
room={room}
roomCategories={roomCategories}
/>
</div>
<div className={styles.roomSelectionPanel}>
<RoomSelectionPanel
availablePackages={availablePackages}
defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={index}
selectedPackages={selectedPackagesByRoom[index]}
/>
</div>
</div>
</div>
)
})
) : (
<RoomSelectionPanel
availablePackages={availablePackages}
defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={0}
selectedPackages={selectedPackagesByRoom[0]}
/>
)}
{rateSummary && (
<form
method="GET"
action={`details?${searchParams}`}
onSubmit={handleSubmit}
>
<RateSummary
isUserLoggedIn={isUserLoggedIn}
packages={availablePackages}
roomsAvailability={roomsAvailability}
booking={bookingWidgetSearchData}
vat={vat}
/>
</form>
)}
</div>
)
}