Feat/SW-965 select rate modify room * feat(SW-965): added new state for modify room and smaller fixes * feat(SW-965): update state handling of modifyRateIndex * fix: adjust scroll animation to handle modifyRateIndex * fix: room state logic and removed unused css class Approved-by: Pontus Dreij Approved-by: Arvid Norlin
326 lines
9.9 KiB
TypeScript
326 lines
9.9 KiB
TypeScript
"use client"
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { useCallback, useEffect, useMemo, useTransition } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
|
import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering"
|
|
|
|
import { ChevronDownSmallIcon } from "@/components/Icons"
|
|
import Button from "@/components/TempDesignSystem/Button"
|
|
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 searchParams = useSearchParams()
|
|
const intl = useIntl()
|
|
const [isPending, startTransition] = useTransition()
|
|
|
|
const hotelId = searchParams.get("hotel")
|
|
const arrivalDate = searchParams.get("fromDate")
|
|
const departureDate = searchParams.get("toDate")
|
|
|
|
const {
|
|
selectedRates,
|
|
rateSummary,
|
|
calculateRateSummary,
|
|
initializeRates,
|
|
setGuestsInRooms,
|
|
modifyRateIndex,
|
|
closeModifyRate,
|
|
} = 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(() => {
|
|
if (roomsAvailability) {
|
|
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(() => {
|
|
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])
|
|
|
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault()
|
|
startTransition(() => {
|
|
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)
|
|
router.push(`details?${newSearchParams}`)
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
requestAnimationFrame(() => {
|
|
const SCROLL_OFFSET = 100
|
|
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
|
|
|
|
let targetIndex: number
|
|
if (modifyRateIndex !== null) {
|
|
targetIndex = modifyRateIndex
|
|
} else {
|
|
const index = selectedRates.findIndex((rate) => rate === undefined)
|
|
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, modifyRateIndex])
|
|
|
|
const getRoomState = useCallback(
|
|
(index: number) => {
|
|
const isFirstRoom = index === 0
|
|
const hasPrevRoomBeenSelected = selectedRates[index - 1] !== undefined
|
|
const isCurrentRoomSelected = selectedRates[index] !== undefined
|
|
const isModifyRoom = modifyRateIndex === index
|
|
|
|
if (isModifyRoom && isCurrentRoomSelected) {
|
|
return { active: true, selected: false }
|
|
}
|
|
|
|
if (isCurrentRoomSelected) {
|
|
return { active: false, selected: true }
|
|
}
|
|
|
|
if (
|
|
(isFirstRoom || hasPrevRoomBeenSelected) &&
|
|
modifyRateIndex === null
|
|
) {
|
|
return { active: true, selected: false }
|
|
}
|
|
|
|
return { active: false, selected: false }
|
|
},
|
|
[modifyRateIndex, selectedRates]
|
|
)
|
|
|
|
return (
|
|
<div className={styles.content}>
|
|
{isMultipleRooms ? (
|
|
bookingWidgetSearchData.rooms.map((room, index) => {
|
|
const roomState = getRoomState(index)
|
|
const classNames = roomSelectionPanelVariants(roomState)
|
|
|
|
return (
|
|
<div key={index} className={styles.roomContainer}>
|
|
{!roomState.selected && (
|
|
<header className={styles.header}>
|
|
<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>
|
|
{modifyRateIndex === index ? (
|
|
<Button
|
|
variant="icon"
|
|
size="medium"
|
|
intent="text"
|
|
theme="base"
|
|
onClick={closeModifyRate}
|
|
>
|
|
{intl.formatMessage({ id: "Close" })}
|
|
<ChevronDownSmallIcon />
|
|
</Button>
|
|
) : null}
|
|
</header>
|
|
)}
|
|
|
|
<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 && roomsAvailability && (
|
|
<form
|
|
method="GET"
|
|
action={`details?${searchParams}`}
|
|
onSubmit={handleSubmit}
|
|
>
|
|
<RateSummary
|
|
isUserLoggedIn={isUserLoggedIn}
|
|
packages={availablePackages}
|
|
roomsAvailability={roomsAvailability}
|
|
booking={bookingWidgetSearchData}
|
|
vat={vat}
|
|
/>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|