feat(SW-718) updates after PR comments

This commit is contained in:
Pontus Dreij
2025-01-27 17:08:57 +01:00
parent bfdc62d263
commit 68d7e869db
29 changed files with 371 additions and 321 deletions

View File

@@ -1,6 +1,6 @@
import SkeletonShimmer from "@/components/SkeletonShimmer"
import { RoomCardSkeleton } from "../../SelectRate/RoomList/RoomCard/RoomCardSkeleton"
import { RoomCardSkeleton } from "../RoomCardSkeleton/RoomCardSkeleton"
import styles from "./SelectHotelMapContainerSkeleton.module.css"

View File

@@ -8,7 +8,7 @@ import { selectHotel } from "@/constants/routes/hotelReservation"
import { useHotelFilterStore } from "@/stores/hotel-filters"
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 InteractiveMap from "@/components/Maps/InteractiveMap"
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
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 SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
@@ -34,12 +34,12 @@ export default function RateSummary({
return () => clearTimeout(timer)
}, [])
if (rateSummary.length === 0) return null
const selectedRateSummary = rateSummary.filter(
(summary): summary is Rate => summary !== null
)
if (selectedRateSummary.length === 0) return null
const {
member,
public: publicRate,

View File

@@ -1,6 +1,10 @@
import RoomFilter from "../RoomFilter"
import RoomList from "../RoomList"
import { useSearchParams } from "next/navigation"
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"
export function RoomSelectionPanel({
@@ -13,15 +17,27 @@ export function RoomSelectionPanel({
defaultPackages,
roomListIndex,
}: 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 (
<>
<RoomFilter
<RoomTypeFilter
numberOfRooms={rooms.roomConfigurations.length}
onFilter={handleFilter}
filterOptions={defaultPackages}
roomListIndex={roomListIndex}
initialFilterValues={initialFilterValues}
/>
<RoomList
<RoomTypeList
roomsAvailability={rooms}
roomCategories={roomCategories}
availablePackages={availablePackages}

View File

@@ -1,7 +1,6 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -27,24 +26,12 @@ export default function RoomFilter({
numberOfRooms,
onFilter,
filterOptions,
roomListIndex,
initialFilterValues,
}: RoomFilterProps) {
const isTabletAndUp = useMediaQuery("(min-width: 768px)")
const [isAboveMobile, setIsAboveMobile] = useState(false)
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(() => {
onFilterRef.current = onFilter
}, [onFilter])

View File

@@ -1,11 +1,7 @@
"use client"
import { useSearchParams } from "next/navigation"
import { useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { useRateSelectionStore } from "@/stores/rate-selection"
import { CheckIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
@@ -24,60 +20,11 @@ export default function FlexibilityOption({
name,
paymentTerm,
priceInformation,
roomTypeCode,
petRoomPackage,
roomListIndex,
isSelected,
onSelect,
}: FlexibilityOptionProps) {
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) {
return (
@@ -100,31 +47,14 @@ export default function FlexibilityOption({
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 (
<label>
<input
type="radio"
name={`rateCode-${roomListIndex}`}
name={`rateCode-${product.productType.public.rateCode}`}
value={publicPrice?.rateCode}
onClick={onClick}
ref={inputElementRef}
checked={isSelected}
onChange={onSelect}
/>
<div className={styles.card}>
<div className={styles.header}>

View File

@@ -1,9 +1,10 @@
"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 { useRateSelectionStore } from "@/stores/rate-selection"
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
@@ -34,22 +35,27 @@ export default function RoomCard({
roomListIndex,
}: RoomCardProps) {
const intl = useIntl()
const searchParams = useSearchParams()
const { selectRate, selectedRates } = useRateSelectionStore()
const selectedRate = useRateSelectionStore(
(state) => state.selectedRates[roomListIndex]
)
const rates = {
saveRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "NotCancellable"
),
changeRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "Changeable"
),
flexRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "CancellableBefore6PM"
),
}
const rates = useMemo(
() => ({
saveRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "NotCancellable"
),
changeRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "Changeable"
),
flexRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "CancellableBefore6PM"
),
}),
[rateDefinitions]
)
function findProductForRate(rate: RateDefinition | undefined) {
return rate
@@ -116,6 +122,69 @@ export default function RoomCard({
: "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 (
<li className={classNames}>
<div>
@@ -220,7 +289,7 @@ export default function RoomCard({
) : (
Object.entries(rates).map(([key, rate]) => (
<FlexibilityOption
key={`${roomListIndex}-${rate?.rateCode}-${selectedRate?.roomTypeCode || "unselected"}`}
key={`${roomListIndex}-${rate?.rateCode ?? key}-${selectedRate?.roomTypeCode ?? "unselected"}`}
name={rateKey(key)}
value={key.toLowerCase()}
paymentTerm={key === "flexRate" ? payLater : payNow}
@@ -228,7 +297,17 @@ export default function RoomCard({
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
roomTypeCode={roomConfiguration.roomTypeCode}
petRoomPackage={petRoomPackage}
roomListIndex={roomListIndex}
isSelected={
selectedRate?.publicRateCode === rate?.rateCode &&
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
}
onSelect={() =>
handleRateSelection(
rate?.rateCode ?? "",
rateKey(key),
key === "flexRate" ? payLater : payNow
)
}
/>
))
)}

View File

@@ -4,16 +4,16 @@ import RoomCard from "./RoomCard"
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,
roomCategories,
availablePackages,
selectedPackages,
hotelType,
roomListIndex,
}: RoomListProps) {
}: RoomTypeListProps) {
const { roomConfigurations, rateDefinitions } = roomsAvailability
return (

View File

@@ -1,4 +1,4 @@
import { RoomCardSkeleton } from "../RoomList/RoomCard/RoomCardSkeleton"
import { RoomCardSkeleton } from "../../SelectHotel/RoomCardSkeleton/RoomCardSkeleton"
import styles from "./RoomsContainerSkeleton.module.css"

View File

@@ -4,7 +4,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo } from "react"
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 { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
@@ -15,6 +15,7 @@ import RateSummary from "../RateSummary"
import { RoomSelectionPanel } from "../RoomSelectionPanel"
import SelectedRoomPanel from "../SelectedRoomPanel"
import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
import { roomSelectionPanelVariants } from "./variants"
import styles from "./rooms.module.css"
@@ -41,13 +42,8 @@ export default function Rooms({
const arrivalDate = searchParams.get("fromDate")
const departureDate = searchParams.get("toDate")
const {
modifyRate,
selectedRates,
rateSummary,
calculateRateSummary,
initializeRates,
} = useRateSelectionStore()
const { selectedRates, rateSummary, calculateRateSummary, initializeRates } =
useRateSelectionStore()
const bookingWidgetSearchData = useMemo(
() =>
@@ -117,13 +113,19 @@ export default function Rooms({
useRoomFiltering({ roomsAvailability })
useEffect(() => {
calculateRateSummary({
getFilteredRooms,
availablePackages,
roomCategories,
selectedPackagesByRoom,
})
if (
selectedRates.length > 0 &&
selectedRates.some((rate) => rate !== undefined)
) {
calculateRateSummary({
getFilteredRooms,
availablePackages,
roomCategories,
selectedPackagesByRoom,
})
}
}, [
selectedRates,
getFilteredRooms,
availablePackages,
roomCategories,
@@ -177,11 +179,7 @@ export default function Rooms({
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
window.history.replaceState(
null,
"",
`${pathname}?${queryParams.toString()}`
)
window.history.pushState(null, "", `${pathname}?${queryParams.toString()}`)
router.push(`select-bed?${queryParams}`)
}
@@ -215,58 +213,66 @@ export default function Rooms({
return (
<div className={styles.content}>
{isMultipleRooms ? (
bookingWidgetSearchData.rooms.map((room, index) => (
<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={styles.roomSelectionPanelContainer}
data-selected={selectedRates[index] !== undefined}
data-active-panel={
(index === 0 || selectedRates[index - 1] !== undefined) &&
selectedRates[index] === undefined
}
>
<div className={styles.selectedRoomPanel}>
<SelectedRoomPanel
roomIndex={index}
room={room}
roomCategories={roomCategories}
/>
</div>
<div className={styles.roomSelectionPanel}>
<RoomSelectionPanel
rooms={getRooms(index)}
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]}
hotelType={hotelType}
handleFilter={handleFilterForRoom(index)}
defaultPackages={defaultPackages}
roomListIndex={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}>
{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}
data-selected={selectedRates[index] !== undefined}
data-active-panel={
(index === 0 || selectedRates[index - 1] !== undefined) &&
selectedRates[index] === undefined
}
>
<div className={styles.selectedRoomPanel}>
<SelectedRoomPanel
roomIndex={index}
room={room}
roomCategories={roomCategories}
/>
</div>
<div className={styles.roomSelectionPanel}>
<RoomSelectionPanel
rooms={getRooms(index)}
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]}
hotelType={hotelType}
handleFilter={handleFilterForRoom(index)}
defaultPackages={defaultPackages}
roomListIndex={index}
/>
</div>
</div>
</div>
</div>
))
)
})
) : (
<RoomSelectionPanel
rooms={getRooms(0)}

View File

@@ -15,45 +15,39 @@
padding: var(--Spacing-x2);
}
.roomSelectionPanel {
._basePanel {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
opacity 0.3s ease,
grid-template-rows 0.5s ease;
height: 0;
gap: var(--Spacing-x2);
}
.roomSelectionPanel > * {
overflow: hidden;
}
.selectedRoomPanel {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
opacity 0.3s ease,
grid-template-rows 0.3s ease;
height: 0;
}
.selectedRoomPanel > * {
._basePanel > * {
overflow: hidden;
}
.roomSelectionPanelContainer[data-selected="true"] .selectedRoomPanel {
.roomSelectionPanel {
composes: _basePanel;
gap: var(--Spacing-x2);
}
.selectedRoomPanel {
composes: _basePanel;
}
.roomSelectionPanelContainer.selected .selectedRoomPanel {
grid-template-rows: 1fr;
opacity: 1;
height: auto;
}
.roomSelectionPanelContainer[data-selected="true"] .roomSelectionPanel {
.roomSelectionPanelContainer.selected .roomSelectionPanel {
display: none;
}
.roomSelectionPanelContainer[data-active-panel="true"] .roomSelectionPanel {
.roomSelectionPanelContainer.active .roomSelectionPanel {
grid-template-rows: 1fr;
opacity: 1;
height: auto;

View 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,
},
}
)

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react"
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 Image from "@/components/Image"
@@ -28,6 +28,7 @@ export default function SelectedRoomPanel({
}: SelectedRoomPanelProps) {
const intl = useIntl()
const { rateSummary, modifyRate } = useRateSelectionStore()
const selectedRate = rateSummary[roomIndex]
const images = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.some(