feat(SW-718) updates after PR comments
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
|
|
||||||
import { RoomCardSkeleton } from "../../SelectRate/RoomList/RoomCard/RoomCardSkeleton"
|
import { RoomCardSkeleton } from "../RoomCardSkeleton/RoomCardSkeleton"
|
||||||
|
|
||||||
import styles from "./SelectHotelMapContainerSkeleton.module.css"
|
import styles from "./SelectHotelMapContainerSkeleton.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { selectHotel } from "@/constants/routes/hotelReservation"
|
|||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { RoomCardSkeleton } from "@/components/HotelReservation/SelectRate/RoomList/RoomCard/RoomCardSkeleton"
|
import { RoomCardSkeleton } from "@/components/HotelReservation/SelectHotel/RoomCardSkeleton/RoomCardSkeleton"
|
||||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
import { useRateSelectionStore } from "@/stores/rate-selection"
|
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
||||||
|
|
||||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||||
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
|
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
|
||||||
@@ -34,12 +34,12 @@ export default function RateSummary({
|
|||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (rateSummary.length === 0) return null
|
|
||||||
|
|
||||||
const selectedRateSummary = rateSummary.filter(
|
const selectedRateSummary = rateSummary.filter(
|
||||||
(summary): summary is Rate => summary !== null
|
(summary): summary is Rate => summary !== null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (selectedRateSummary.length === 0) return null
|
||||||
|
|
||||||
const {
|
const {
|
||||||
member,
|
member,
|
||||||
public: publicRate,
|
public: publicRate,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import RoomFilter from "../RoomFilter"
|
import { useSearchParams } from "next/navigation"
|
||||||
import RoomList from "../RoomList"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
import RoomTypeFilter from "../RoomTypeFilter"
|
||||||
|
import RoomTypeList from "../RoomTypeList"
|
||||||
|
|
||||||
|
import type { FilterValues } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export function RoomSelectionPanel({
|
export function RoomSelectionPanel({
|
||||||
@@ -13,15 +17,27 @@ export function RoomSelectionPanel({
|
|||||||
defaultPackages,
|
defaultPackages,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
}: RoomSelectionPanelProps) {
|
}: RoomSelectionPanelProps) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
const initialFilterValues = useMemo(() => {
|
||||||
|
const packagesFromSearchParams =
|
||||||
|
searchParams.get(`room[${roomListIndex}].packages`)?.split(",") ?? []
|
||||||
|
|
||||||
|
return defaultPackages.reduce<FilterValues>((acc, option) => {
|
||||||
|
acc[option.code] = packagesFromSearchParams.includes(option.code)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}, [defaultPackages, searchParams, roomListIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RoomFilter
|
<RoomTypeFilter
|
||||||
numberOfRooms={rooms.roomConfigurations.length}
|
numberOfRooms={rooms.roomConfigurations.length}
|
||||||
onFilter={handleFilter}
|
onFilter={handleFilter}
|
||||||
filterOptions={defaultPackages}
|
filterOptions={defaultPackages}
|
||||||
roomListIndex={roomListIndex}
|
initialFilterValues={initialFilterValues}
|
||||||
/>
|
/>
|
||||||
<RoomList
|
<RoomTypeList
|
||||||
roomsAvailability={rooms}
|
roomsAvailability={rooms}
|
||||||
roomCategories={roomCategories}
|
roomCategories={roomCategories}
|
||||||
availablePackages={availablePackages}
|
availablePackages={availablePackages}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -27,24 +26,12 @@ export default function RoomFilter({
|
|||||||
numberOfRooms,
|
numberOfRooms,
|
||||||
onFilter,
|
onFilter,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
roomListIndex,
|
initialFilterValues,
|
||||||
}: RoomFilterProps) {
|
}: RoomFilterProps) {
|
||||||
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
|
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
|
||||||
const [isAboveMobile, setIsAboveMobile] = useState(false)
|
const [isAboveMobile, setIsAboveMobile] = useState(false)
|
||||||
const onFilterRef = useRef(onFilter)
|
const onFilterRef = useRef(onFilter)
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
const initialFilterValues = useMemo(() => {
|
|
||||||
const packagesFromSearchParams =
|
|
||||||
searchParams.get(`room[${roomListIndex}].packages`)?.split(",") ?? []
|
|
||||||
|
|
||||||
return filterOptions.reduce<FilterValues>((acc, option) => {
|
|
||||||
acc[option.code] = packagesFromSearchParams.includes(option.code)
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}, [filterOptions, searchParams, roomListIndex])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFilterRef.current = onFilter
|
onFilterRef.current = onFilter
|
||||||
}, [onFilter])
|
}, [onFilter])
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { useEffect, useRef } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useRateSelectionStore } from "@/stores/rate-selection"
|
|
||||||
|
|
||||||
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
@@ -24,60 +20,11 @@ export default function FlexibilityOption({
|
|||||||
name,
|
name,
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
priceInformation,
|
priceInformation,
|
||||||
roomTypeCode,
|
|
||||||
petRoomPackage,
|
petRoomPackage,
|
||||||
roomListIndex,
|
isSelected,
|
||||||
|
onSelect,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const inputElementRef = useRef<HTMLInputElement>(null)
|
|
||||||
const { selectRate, selectedRates } = useRateSelectionStore()
|
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
|
|
||||||
// When entering the page with a room and rate selection already in the URL we
|
|
||||||
// want to preselect the selection. This happens e.g. when you do a selection,
|
|
||||||
// go to the enter details page and then want to change the room.
|
|
||||||
useEffect(() => {
|
|
||||||
const ratecodeSearchParam = searchParams.get(
|
|
||||||
`room[${roomListIndex}].ratecode`
|
|
||||||
)
|
|
||||||
const roomtypeSearchParam = searchParams.get(
|
|
||||||
`room[${roomListIndex}].roomtype`
|
|
||||||
)
|
|
||||||
|
|
||||||
// If this is not the room and rate we want to preselect, abort
|
|
||||||
if (
|
|
||||||
!product ||
|
|
||||||
ratecodeSearchParam !== product.productType.public.rateCode ||
|
|
||||||
roomtypeSearchParam !== roomTypeCode
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's already a selection for this room index
|
|
||||||
const existingSelection = selectedRates[roomListIndex]
|
|
||||||
if (existingSelection) return
|
|
||||||
|
|
||||||
selectRate(roomListIndex, {
|
|
||||||
publicRateCode: product.productType.public.rateCode,
|
|
||||||
roomTypeCode: roomTypeCode,
|
|
||||||
name: name,
|
|
||||||
paymentTerm: paymentTerm,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (inputElementRef.current) {
|
|
||||||
inputElementRef.current.checked = true
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
searchParams,
|
|
||||||
roomListIndex,
|
|
||||||
product,
|
|
||||||
roomTypeCode,
|
|
||||||
name,
|
|
||||||
paymentTerm,
|
|
||||||
selectedRates,
|
|
||||||
selectRate,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return (
|
return (
|
||||||
@@ -100,31 +47,14 @@ export default function FlexibilityOption({
|
|||||||
|
|
||||||
const { public: publicPrice, member: memberPrice } = product.productType
|
const { public: publicPrice, member: memberPrice } = product.productType
|
||||||
|
|
||||||
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
if (
|
|
||||||
selectedRates[roomListIndex]?.publicRateCode === publicPrice.rateCode &&
|
|
||||||
selectedRates[roomListIndex]?.roomTypeCode === roomTypeCode
|
|
||||||
) {
|
|
||||||
if (e.currentTarget?.checked) e.currentTarget.checked = false
|
|
||||||
selectRate(roomListIndex, undefined)
|
|
||||||
} else {
|
|
||||||
selectRate(roomListIndex, {
|
|
||||||
publicRateCode: publicPrice.rateCode,
|
|
||||||
roomTypeCode: roomTypeCode,
|
|
||||||
name: name,
|
|
||||||
paymentTerm: paymentTerm,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name={`rateCode-${roomListIndex}`}
|
name={`rateCode-${product.productType.public.rateCode}`}
|
||||||
value={publicPrice?.rateCode}
|
value={publicPrice?.rateCode}
|
||||||
onClick={onClick}
|
checked={isSelected}
|
||||||
ref={inputElementRef}
|
onChange={onSelect}
|
||||||
/>
|
/>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { createElement, useCallback } from "react"
|
import { useSearchParams } from "next/navigation"
|
||||||
|
import { createElement, useCallback, useEffect, useMemo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useRateSelectionStore } from "@/stores/rate-selection"
|
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
||||||
|
|
||||||
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
|
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
|
||||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||||
@@ -34,12 +35,15 @@ export default function RoomCard({
|
|||||||
roomListIndex,
|
roomListIndex,
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const { selectRate, selectedRates } = useRateSelectionStore()
|
||||||
|
|
||||||
const selectedRate = useRateSelectionStore(
|
const selectedRate = useRateSelectionStore(
|
||||||
(state) => state.selectedRates[roomListIndex]
|
(state) => state.selectedRates[roomListIndex]
|
||||||
)
|
)
|
||||||
|
|
||||||
const rates = {
|
const rates = useMemo(
|
||||||
|
() => ({
|
||||||
saveRate: rateDefinitions.find(
|
saveRate: rateDefinitions.find(
|
||||||
(rate) => rate.cancellationRule === "NotCancellable"
|
(rate) => rate.cancellationRule === "NotCancellable"
|
||||||
),
|
),
|
||||||
@@ -49,7 +53,9 @@ export default function RoomCard({
|
|||||||
flexRate: rateDefinitions.find(
|
flexRate: rateDefinitions.find(
|
||||||
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
(rate) => rate.cancellationRule === "CancellableBefore6PM"
|
||||||
),
|
),
|
||||||
}
|
}),
|
||||||
|
[rateDefinitions]
|
||||||
|
)
|
||||||
|
|
||||||
function findProductForRate(rate: RateDefinition | undefined) {
|
function findProductForRate(rate: RateDefinition | undefined) {
|
||||||
return rate
|
return rate
|
||||||
@@ -116,6 +122,69 @@ export default function RoomCard({
|
|||||||
: "default",
|
: "default",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle URL-based preselection
|
||||||
|
useEffect(() => {
|
||||||
|
const ratecodeSearchParam = searchParams.get(
|
||||||
|
`room[${roomListIndex}].ratecode`
|
||||||
|
)
|
||||||
|
const roomtypeSearchParam = searchParams.get(
|
||||||
|
`room[${roomListIndex}].roomtype`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!ratecodeSearchParam || !roomtypeSearchParam) return
|
||||||
|
|
||||||
|
// Check if there's already a selection for this room index
|
||||||
|
const existingSelection = selectedRates[roomListIndex]
|
||||||
|
if (existingSelection) return
|
||||||
|
|
||||||
|
const matchingRate = Object.entries(rates).find(
|
||||||
|
([_, rate]) =>
|
||||||
|
rate?.rateCode === ratecodeSearchParam &&
|
||||||
|
roomConfiguration.roomTypeCode === roomtypeSearchParam
|
||||||
|
)
|
||||||
|
|
||||||
|
if (matchingRate) {
|
||||||
|
const [key, rate] = matchingRate
|
||||||
|
selectRate(roomListIndex, {
|
||||||
|
publicRateCode: rate?.rateCode ?? "",
|
||||||
|
roomTypeCode: roomConfiguration.roomTypeCode,
|
||||||
|
name: rateKey(key),
|
||||||
|
paymentTerm: key === "flexRate" ? payLater : payNow,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
searchParams,
|
||||||
|
roomListIndex,
|
||||||
|
rates,
|
||||||
|
roomConfiguration.roomTypeCode,
|
||||||
|
payLater,
|
||||||
|
payNow,
|
||||||
|
selectRate,
|
||||||
|
selectedRates,
|
||||||
|
rateKey,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleRateSelection = (
|
||||||
|
rateCode: string,
|
||||||
|
rateName: string,
|
||||||
|
paymentTerm: string
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
selectedRates[roomListIndex]?.publicRateCode === rateCode &&
|
||||||
|
selectedRates[roomListIndex]?.roomTypeCode ===
|
||||||
|
roomConfiguration.roomTypeCode
|
||||||
|
) {
|
||||||
|
selectRate(roomListIndex, undefined)
|
||||||
|
} else {
|
||||||
|
selectRate(roomListIndex, {
|
||||||
|
publicRateCode: rateCode,
|
||||||
|
roomTypeCode: roomConfiguration.roomTypeCode,
|
||||||
|
name: rateName,
|
||||||
|
paymentTerm: paymentTerm,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={classNames}>
|
<li className={classNames}>
|
||||||
<div>
|
<div>
|
||||||
@@ -220,7 +289,7 @@ export default function RoomCard({
|
|||||||
) : (
|
) : (
|
||||||
Object.entries(rates).map(([key, rate]) => (
|
Object.entries(rates).map(([key, rate]) => (
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
key={`${roomListIndex}-${rate?.rateCode}-${selectedRate?.roomTypeCode || "unselected"}`}
|
key={`${roomListIndex}-${rate?.rateCode ?? key}-${selectedRate?.roomTypeCode ?? "unselected"}`}
|
||||||
name={rateKey(key)}
|
name={rateKey(key)}
|
||||||
value={key.toLowerCase()}
|
value={key.toLowerCase()}
|
||||||
paymentTerm={key === "flexRate" ? payLater : payNow}
|
paymentTerm={key === "flexRate" ? payLater : payNow}
|
||||||
@@ -228,7 +297,17 @@ export default function RoomCard({
|
|||||||
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
|
||||||
roomTypeCode={roomConfiguration.roomTypeCode}
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
petRoomPackage={petRoomPackage}
|
petRoomPackage={petRoomPackage}
|
||||||
roomListIndex={roomListIndex}
|
isSelected={
|
||||||
|
selectedRate?.publicRateCode === rate?.rateCode &&
|
||||||
|
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
|
||||||
|
}
|
||||||
|
onSelect={() =>
|
||||||
|
handleRateSelection(
|
||||||
|
rate?.rateCode ?? "",
|
||||||
|
rateKey(key),
|
||||||
|
key === "flexRate" ? payLater : payNow
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -4,16 +4,16 @@ import RoomCard from "./RoomCard"
|
|||||||
|
|
||||||
import styles from "./roomSelection.module.css"
|
import styles from "./roomSelection.module.css"
|
||||||
|
|
||||||
import type { RoomListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export default function RoomList({
|
export default function RoomTypeList({
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
availablePackages,
|
availablePackages,
|
||||||
selectedPackages,
|
selectedPackages,
|
||||||
hotelType,
|
hotelType,
|
||||||
roomListIndex,
|
roomListIndex,
|
||||||
}: RoomListProps) {
|
}: RoomTypeListProps) {
|
||||||
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
const { roomConfigurations, rateDefinitions } = roomsAvailability
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RoomCardSkeleton } from "../RoomList/RoomCard/RoomCardSkeleton"
|
import { RoomCardSkeleton } from "../../SelectHotel/RoomCardSkeleton/RoomCardSkeleton"
|
||||||
|
|
||||||
import styles from "./RoomsContainerSkeleton.module.css"
|
import styles from "./RoomsContainerSkeleton.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|||||||
import { useCallback, useEffect, useMemo } from "react"
|
import { useCallback, useEffect, useMemo } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useRateSelectionStore } from "@/stores/rate-selection"
|
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
||||||
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
|
import { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
|
||||||
@@ -15,6 +15,7 @@ import RateSummary from "../RateSummary"
|
|||||||
import { RoomSelectionPanel } from "../RoomSelectionPanel"
|
import { RoomSelectionPanel } from "../RoomSelectionPanel"
|
||||||
import SelectedRoomPanel from "../SelectedRoomPanel"
|
import SelectedRoomPanel from "../SelectedRoomPanel"
|
||||||
import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
|
import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
|
||||||
|
import { roomSelectionPanelVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
@@ -41,13 +42,8 @@ export default function Rooms({
|
|||||||
const arrivalDate = searchParams.get("fromDate")
|
const arrivalDate = searchParams.get("fromDate")
|
||||||
const departureDate = searchParams.get("toDate")
|
const departureDate = searchParams.get("toDate")
|
||||||
|
|
||||||
const {
|
const { selectedRates, rateSummary, calculateRateSummary, initializeRates } =
|
||||||
modifyRate,
|
useRateSelectionStore()
|
||||||
selectedRates,
|
|
||||||
rateSummary,
|
|
||||||
calculateRateSummary,
|
|
||||||
initializeRates,
|
|
||||||
} = useRateSelectionStore()
|
|
||||||
|
|
||||||
const bookingWidgetSearchData = useMemo(
|
const bookingWidgetSearchData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -117,13 +113,19 @@ export default function Rooms({
|
|||||||
useRoomFiltering({ roomsAvailability })
|
useRoomFiltering({ roomsAvailability })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedRates.length > 0 &&
|
||||||
|
selectedRates.some((rate) => rate !== undefined)
|
||||||
|
) {
|
||||||
calculateRateSummary({
|
calculateRateSummary({
|
||||||
getFilteredRooms,
|
getFilteredRooms,
|
||||||
availablePackages,
|
availablePackages,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
selectedPackagesByRoom,
|
selectedPackagesByRoom,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
|
selectedRates,
|
||||||
getFilteredRooms,
|
getFilteredRooms,
|
||||||
availablePackages,
|
availablePackages,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
@@ -177,11 +179,7 @@ export default function Rooms({
|
|||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
window.history.replaceState(
|
window.history.pushState(null, "", `${pathname}?${queryParams.toString()}`)
|
||||||
null,
|
|
||||||
"",
|
|
||||||
`${pathname}?${queryParams.toString()}`
|
|
||||||
)
|
|
||||||
router.push(`select-bed?${queryParams}`)
|
router.push(`select-bed?${queryParams}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +213,14 @@ export default function Rooms({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{isMultipleRooms ? (
|
{isMultipleRooms ? (
|
||||||
bookingWidgetSearchData.rooms.map((room, index) => (
|
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}>
|
<div key={index} className={styles.roomContainer}>
|
||||||
{selectedRates[index] === undefined && (
|
{selectedRates[index] === undefined && (
|
||||||
<Subtitle>
|
<Subtitle>
|
||||||
@@ -238,7 +243,7 @@ export default function Rooms({
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={styles.roomSelectionPanelContainer}
|
className={classNames}
|
||||||
data-selected={selectedRates[index] !== undefined}
|
data-selected={selectedRates[index] !== undefined}
|
||||||
data-active-panel={
|
data-active-panel={
|
||||||
(index === 0 || selectedRates[index - 1] !== undefined) &&
|
(index === 0 || selectedRates[index - 1] !== undefined) &&
|
||||||
@@ -266,7 +271,8 @@ export default function Rooms({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<RoomSelectionPanel
|
<RoomSelectionPanel
|
||||||
rooms={getRooms(0)}
|
rooms={getRooms(0)}
|
||||||
|
|||||||
@@ -15,45 +15,39 @@
|
|||||||
padding: var(--Spacing-x2);
|
padding: var(--Spacing-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomSelectionPanel {
|
._basePanel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 0fr;
|
grid-template-rows: 0fr;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition:
|
|
||||||
opacity 0.3s ease,
|
|
||||||
grid-template-rows 0.5s ease;
|
|
||||||
height: 0;
|
height: 0;
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.roomSelectionPanel > * {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectedRoomPanel {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
opacity: 0;
|
|
||||||
transition:
|
transition:
|
||||||
opacity 0.3s ease,
|
opacity 0.3s ease,
|
||||||
grid-template-rows 0.3s ease;
|
grid-template-rows 0.3s ease;
|
||||||
height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectedRoomPanel > * {
|
._basePanel > * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomSelectionPanelContainer[data-selected="true"] .selectedRoomPanel {
|
.roomSelectionPanel {
|
||||||
|
composes: _basePanel;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedRoomPanel {
|
||||||
|
composes: _basePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomSelectionPanelContainer.selected .selectedRoomPanel {
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.roomSelectionPanelContainer[data-selected="true"] .roomSelectionPanel {
|
.roomSelectionPanelContainer.selected .roomSelectionPanel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.roomSelectionPanelContainer[data-active-panel="true"] .roomSelectionPanel {
|
.roomSelectionPanelContainer.active .roomSelectionPanel {
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
21
components/HotelReservation/SelectRate/Rooms/variants.ts
Normal file
21
components/HotelReservation/SelectRate/Rooms/variants.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
|
export const roomSelectionPanelVariants = cva(
|
||||||
|
styles.roomSelectionPanelContainer,
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
active: {
|
||||||
|
true: styles.active,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
true: styles.selected,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
active: false,
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useCallback } from "react"
|
import { useCallback } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { useRateSelectionStore } from "@/stores/rate-selection"
|
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
|
||||||
|
|
||||||
import { EditIcon } from "@/components/Icons"
|
import { EditIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
@@ -28,6 +28,7 @@ export default function SelectedRoomPanel({
|
|||||||
}: SelectedRoomPanelProps) {
|
}: SelectedRoomPanelProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { rateSummary, modifyRate } = useRateSelectionStore()
|
const { rateSummary, modifyRate } = useRateSelectionStore()
|
||||||
|
|
||||||
const selectedRate = rateSummary[roomIndex]
|
const selectedRate = rateSummary[roomIndex]
|
||||||
const images = roomCategories.find((roomCategory) =>
|
const images = roomCategories.find((roomCategory) =>
|
||||||
roomCategory.roomTypes.some(
|
roomCategory.roomTypes.some(
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { create } from "zustand"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
RoomPackageCodeEnum,
|
|
||||||
RoomPackageData} from "@/types/components/hotelReservation/selectRate/roomFilter";
|
|
||||||
import type { RoomParam } from "@/types/components/hotelReservation/selectRate/section"
|
|
||||||
import type {
|
|
||||||
Rate,
|
|
||||||
RateCode,
|
|
||||||
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
|
||||||
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
|
||||||
|
|
||||||
interface RateSummaryParams {
|
|
||||||
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
|
||||||
availablePackages: RoomPackageData
|
|
||||||
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
|
||||||
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
|
||||||
}
|
|
||||||
interface RateSelectionState {
|
|
||||||
selectedRates: (RateCode | undefined)[]
|
|
||||||
roomsAndGuests: RoomParam[]
|
|
||||||
rateSummary: (Rate | null)[]
|
|
||||||
modifyRate: (index: number) => void
|
|
||||||
selectRate: (index: number, rate: RateCode | undefined) => void
|
|
||||||
setRoomsAndGuests: (rooms: RoomParam[]) => void
|
|
||||||
initializeRates: (count: number) => void
|
|
||||||
calculateRateSummary: ({
|
|
||||||
getFilteredRooms,
|
|
||||||
availablePackages,
|
|
||||||
roomCategories,
|
|
||||||
}: RateSummaryParams) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useRateSelectionStore = create<RateSelectionState>((set, get) => ({
|
|
||||||
selectedRates: [],
|
|
||||||
roomsAndGuests: [],
|
|
||||||
rateSummary: [],
|
|
||||||
modifyRate: (index) =>
|
|
||||||
set((state) => {
|
|
||||||
const newRates = [...state.selectedRates]
|
|
||||||
newRates[index] = undefined
|
|
||||||
return { selectedRates: newRates }
|
|
||||||
}),
|
|
||||||
selectRate: (index, rate) =>
|
|
||||||
set((state) => {
|
|
||||||
const newRates = [...state.selectedRates]
|
|
||||||
newRates[index] = rate
|
|
||||||
return { selectedRates: newRates }
|
|
||||||
}),
|
|
||||||
initializeRates: (count) =>
|
|
||||||
set({ selectedRates: new Array(count).fill(undefined) }),
|
|
||||||
setRoomsAndGuests: (rooms) => set({ roomsAndGuests: rooms }),
|
|
||||||
calculateRateSummary: ({
|
|
||||||
getFilteredRooms,
|
|
||||||
availablePackages,
|
|
||||||
roomCategories,
|
|
||||||
selectedPackagesByRoom,
|
|
||||||
}) => {
|
|
||||||
const state = get()
|
|
||||||
const summaries = state.roomsAndGuests.map((_, roomIndex) => {
|
|
||||||
const selectedRate = state.selectedRates[roomIndex]
|
|
||||||
const filteredRooms = getFilteredRooms(roomIndex)
|
|
||||||
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
|
||||||
|
|
||||||
const room = filteredRooms.find(
|
|
||||||
(room) => room.roomTypeCode === selectedRate?.roomTypeCode
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!room) return null
|
|
||||||
|
|
||||||
const product = room.products.find(
|
|
||||||
(product) =>
|
|
||||||
product.productType.public.rateCode === selectedRate?.publicRateCode
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!product) return null
|
|
||||||
|
|
||||||
const petRoomPackage =
|
|
||||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
|
||||||
availablePackages.find(
|
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)) ||
|
|
||||||
undefined
|
|
||||||
|
|
||||||
const features = filteredRooms.find((room) =>
|
|
||||||
room.features.some(
|
|
||||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
|
||||||
)
|
|
||||||
)?.features
|
|
||||||
|
|
||||||
const roomType = roomCategories.find((roomCategory) =>
|
|
||||||
roomCategory.roomTypes.some(
|
|
||||||
(roomType) => roomType.code === room.roomTypeCode
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: petRoomPackage && features ? features : [],
|
|
||||||
priceName: selectedRate?.name,
|
|
||||||
priceTerm: selectedRate?.paymentTerm,
|
|
||||||
public: product.productType.public,
|
|
||||||
member: product.productType.member,
|
|
||||||
roomType: roomType?.name ?? room.roomType,
|
|
||||||
roomTypeCode: room.roomTypeCode,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
set({ rateSummary: summaries })
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
58
stores/select-rate/helper.ts
Normal file
58
stores/select-rate/helper.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type {
|
||||||
|
Rate,
|
||||||
|
RateCode,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import type { RateSummaryParams } from "./rate-selection"
|
||||||
|
|
||||||
|
interface CalculateRoomSummaryParams extends RateSummaryParams {
|
||||||
|
selectedRate: RateCode
|
||||||
|
roomIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateRoomSummary({
|
||||||
|
selectedRate,
|
||||||
|
roomIndex,
|
||||||
|
getFilteredRooms,
|
||||||
|
availablePackages,
|
||||||
|
roomCategories,
|
||||||
|
selectedPackagesByRoom,
|
||||||
|
}: CalculateRoomSummaryParams): Rate | null {
|
||||||
|
const filteredRooms = getFilteredRooms(roomIndex)
|
||||||
|
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
|
||||||
|
|
||||||
|
const room = filteredRooms.find(
|
||||||
|
(room) => room.roomTypeCode === selectedRate.roomTypeCode
|
||||||
|
)
|
||||||
|
if (!room) return null
|
||||||
|
|
||||||
|
const product = room.products.find(
|
||||||
|
(product) =>
|
||||||
|
product.productType.public.rateCode === selectedRate.publicRateCode
|
||||||
|
)
|
||||||
|
if (!product) return null
|
||||||
|
|
||||||
|
const petRoomPackage = selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM)
|
||||||
|
? availablePackages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const features = filteredRooms.find((room) =>
|
||||||
|
room.features.some(
|
||||||
|
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
|
)
|
||||||
|
)?.features
|
||||||
|
|
||||||
|
const roomType = roomCategories.find((roomCategory) =>
|
||||||
|
roomCategory.roomTypes.some((type) => type.code === room.roomTypeCode)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
features: petRoomPackage && features ? features : [],
|
||||||
|
priceName: selectedRate.name,
|
||||||
|
priceTerm: selectedRate.paymentTerm,
|
||||||
|
public: product.productType.public,
|
||||||
|
member: product.productType.member,
|
||||||
|
roomType: roomType?.name ?? room.roomType,
|
||||||
|
roomTypeCode: room.roomTypeCode,
|
||||||
|
}
|
||||||
|
}
|
||||||
67
stores/select-rate/rate-selection.ts
Normal file
67
stores/select-rate/rate-selection.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
import { calculateRoomSummary } from "./helper"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RoomPackageCodeEnum,
|
||||||
|
type RoomPackageData,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/roomFilter";
|
||||||
|
import type {
|
||||||
|
Rate,
|
||||||
|
RateCode,
|
||||||
|
} from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
|
export interface RateSummaryParams {
|
||||||
|
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
|
||||||
|
availablePackages: RoomPackageData
|
||||||
|
roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }>
|
||||||
|
selectedPackagesByRoom: Record<number, RoomPackageCodeEnum[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RateSelectionState {
|
||||||
|
selectedRates: (RateCode | undefined)[]
|
||||||
|
rateSummary: (Rate | null)[]
|
||||||
|
modifyRate: (index: number) => void
|
||||||
|
selectRate: (index: number, rate: RateCode | undefined) => void
|
||||||
|
initializeRates: (count: number) => void
|
||||||
|
calculateRateSummary: ({
|
||||||
|
getFilteredRooms,
|
||||||
|
availablePackages,
|
||||||
|
roomCategories,
|
||||||
|
}: RateSummaryParams) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRateSelectionStore = create<RateSelectionState>((set, get) => ({
|
||||||
|
selectedRates: [],
|
||||||
|
rateSummary: [],
|
||||||
|
modifyRate: (index) =>
|
||||||
|
set((state) => {
|
||||||
|
const newRates = [...state.selectedRates]
|
||||||
|
newRates[index] = undefined
|
||||||
|
return { selectedRates: newRates }
|
||||||
|
}),
|
||||||
|
selectRate: (index, rate) =>
|
||||||
|
set((state) => {
|
||||||
|
const newRates = [...state.selectedRates]
|
||||||
|
newRates[index] = rate
|
||||||
|
return { selectedRates: newRates }
|
||||||
|
}),
|
||||||
|
initializeRates: (count) =>
|
||||||
|
set({ selectedRates: new Array(count).fill(undefined) }),
|
||||||
|
calculateRateSummary: (params) => {
|
||||||
|
const { selectedRates } = get()
|
||||||
|
|
||||||
|
const summaries = selectedRates.map((selectedRate, roomIndex) => {
|
||||||
|
if (!selectedRate) return null
|
||||||
|
|
||||||
|
return calculateRoomSummary({
|
||||||
|
selectedRate,
|
||||||
|
roomIndex,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
set({ rateSummary: summaries })
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -19,7 +19,8 @@ export type FlexibilityOptionProps = {
|
|||||||
priceInformation?: Array<string>
|
priceInformation?: Array<string>
|
||||||
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
roomTypeCode: RoomConfiguration["roomTypeCode"]
|
||||||
petRoomPackage: RoomPackage | undefined
|
petRoomPackage: RoomPackage | undefined
|
||||||
roomListIndex: number
|
isSelected: boolean
|
||||||
|
onSelect: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PriceListProps {
|
export interface PriceListProps {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface RoomFilterProps {
|
|||||||
numberOfRooms: number
|
numberOfRooms: number
|
||||||
onFilter: (filter: Record<string, boolean | undefined>) => void
|
onFilter: (filter: Record<string, boolean | undefined>) => void
|
||||||
filterOptions: DefaultFilterOptions[]
|
filterOptions: DefaultFilterOptions[]
|
||||||
roomListIndex: number
|
initialFilterValues: FilterValues
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoomPackage = z.output<typeof packagesSchema>
|
export type RoomPackage = z.output<typeof packagesSchema>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
RoomPackageData,
|
RoomPackageData,
|
||||||
} from "./roomFilter"
|
} from "./roomFilter"
|
||||||
|
|
||||||
export interface RoomListProps {
|
export interface RoomTypeListProps {
|
||||||
roomsAvailability: RoomsAvailability
|
roomsAvailability: RoomsAvailability
|
||||||
roomCategories: RoomData[]
|
roomCategories: RoomData[]
|
||||||
availablePackages: RoomPackageData | undefined
|
availablePackages: RoomPackageData | undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user