feat(SW-718): Created store for selectRate

This commit is contained in:
Pontus Dreij
2025-01-22 10:44:51 +01:00
parent 98793c58e3
commit 2a6c88d897
13 changed files with 168 additions and 89 deletions

View File

@@ -4,6 +4,8 @@ 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,16 +26,11 @@ export default function FlexibilityOption({
priceInformation,
roomTypeCode,
petRoomPackage,
handleSelectRate,
roomListIndex,
}: FlexibilityOptionProps) {
const intl = useIntl()
const inputElementRef = useRef<HTMLInputElement>(null)
const handleSelectRateRef = useRef(handleSelectRate)
useEffect(() => {
handleSelectRateRef.current = handleSelectRate
}, [handleSelectRate])
const { selectRate, selectedRates } = useRateSelectionStore()
const searchParams = useSearchParams()
@@ -57,24 +54,30 @@ export default function FlexibilityOption({
return
}
handleSelectRateRef.current((prev) => {
// If the user already has made a new selection we respect that and don't do anything else
if (prev) {
return prev
}
// Check if there's already a selection for this room index
const existingSelection = selectedRates[roomListIndex]
if (existingSelection) return
if (inputElementRef.current) {
inputElementRef.current.checked = true
}
return {
publicRateCode: product.productType.public.rateCode,
roomTypeCode: roomTypeCode,
name: name,
paymentTerm: paymentTerm,
}
selectRate(roomListIndex, {
publicRateCode: product.productType.public.rateCode,
roomTypeCode: roomTypeCode,
name: name,
paymentTerm: paymentTerm,
})
}, [searchParams, roomListIndex, product, roomTypeCode, name, paymentTerm])
if (inputElementRef.current) {
inputElementRef.current.checked = true
}
}, [
searchParams,
roomListIndex,
product,
roomTypeCode,
name,
paymentTerm,
selectedRates,
selectRate,
])
if (!product) {
return (
@@ -98,22 +101,20 @@ export default function FlexibilityOption({
const { public: publicPrice, member: memberPrice } = product.productType
const onClick: React.MouseEventHandler<HTMLInputElement> = (e) => {
handleSelectRateRef.current((prev) => {
if (
prev &&
prev.publicRateCode === publicPrice.rateCode &&
prev.roomTypeCode === roomTypeCode
) {
if (e.currentTarget?.checked) e.currentTarget.checked = false
return undefined
} else
return {
publicRateCode: publicPrice.rateCode,
roomTypeCode: roomTypeCode,
name: name,
paymentTerm: paymentTerm,
}
})
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 (

View File

@@ -3,6 +3,8 @@
import { createElement, useCallback } from "react"
import { useIntl } from "react-intl"
import { useRateSelectionStore } from "@/stores/rate-selection"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
import { ErrorCircleIcon } from "@/components/Icons"
@@ -29,11 +31,14 @@ export default function RoomCard({
roomCategories,
selectedPackages,
packages,
handleSelectRate,
roomListIndex,
}: RoomCardProps) {
const intl = useIntl()
const selectedRate = useRateSelectionStore(
(state) => state.selectedRates[roomListIndex]
)
const rates = {
saveRate: rateDefinitions.find(
(rate) => rate.cancellationRule === "NotCancellable"
@@ -215,13 +220,12 @@ export default function RoomCard({
) : (
Object.entries(rates).map(([key, rate]) => (
<FlexibilityOption
key={key}
key={`${roomListIndex}-${rate?.rateCode}-${selectedRate?.roomTypeCode || "unselected"}`}
name={rateKey(key)}
value={key.toLowerCase()}
paymentTerm={key === "flexRate" ? payLater : payNow}
product={findProductForRate(rate)}
priceInformation={getRateDefinitionForRate(rate)?.generalTerms}
handleSelectRate={handleSelectRate}
roomTypeCode={roomConfiguration.roomTypeCode}
petRoomPackage={petRoomPackage}
roomListIndex={roomListIndex}

View File

@@ -11,7 +11,6 @@ export default function RoomList({
roomCategories,
availablePackages,
selectedPackages,
setRateCode,
hotelType,
roomListIndex,
}: RoomListProps) {
@@ -27,7 +26,6 @@ export default function RoomList({
rateDefinitions={rateDefinitions}
roomConfiguration={roomConfiguration}
roomCategories={roomCategories}
handleSelectRate={setRateCode}
selectedPackages={selectedPackages}
packages={availablePackages}
key={roomConfiguration.roomTypeCode}

View File

@@ -1,7 +1,3 @@
.wrapper {
padding-bottom: var(--Spacing-x3);
}
.roomList {
list-style: none;
display: grid;

View File

@@ -8,7 +8,6 @@ export function RoomSelectionPanel({
roomCategories,
availablePackages,
selectedPackages,
setSelectedRate,
hotelType,
handleFilter,
defaultPackages,
@@ -27,7 +26,6 @@ export function RoomSelectionPanel({
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackages}
setRateCode={setSelectedRate}
hotelType={hotelType}
roomListIndex={roomListIndex}
/>

View File

@@ -1,10 +1,11 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useMemo } from "react"
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { useRateSelectionStore } from "@/stores/rate-selection"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRateSummary } from "@/hooks/selectRate/useRateSummary"
import { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
@@ -23,14 +24,9 @@ import {
RoomPackageCodeEnum,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
import type {
Rate,
RateCode,
} from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomConfiguration } from "@/server/routers/hotels/output"
type SelectedRates = (RateCode | undefined)[]
export default function Rooms({
roomsAvailability,
roomCategories = [],
@@ -46,19 +42,22 @@ export default function Rooms({
const arrivalDate = searchParams.get("fromDate")
const departureDate = searchParams.get("toDate")
const { modifyRate, selectedRates, setSelectedRates } =
useRateSelectionStore()
const searchedRoomsAndGuests = useMemo(
() => parseRoomParams(searchParams),
[searchParams]
)
const [selectedRates, setSelectedRates] = useState<SelectedRates>(
new Array(searchedRoomsAndGuests.length).fill(undefined)
)
const isMultipleRooms = searchedRoomsAndGuests.length > 1
const intl = useIntl()
useEffect(() => {
setSelectedRates(new Array(searchedRoomsAndGuests.length).fill(undefined))
}, [setSelectedRates, searchedRoomsAndGuests.length])
const visibleRooms: RoomConfiguration[] = useMemo(() => {
const deduped = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
@@ -173,16 +172,11 @@ export default function Rooms({
router.push(`select-bed?${queryParams}`)
}
const setSelectedRateForRoom = useCallback(
(index: number) => (value: React.SetStateAction<RateCode | undefined>) => {
setSelectedRates((prev) => {
const newRates = [...prev]
newRates[index] =
typeof value === "function" ? value(prev[index]) : value
return newRates
})
const handleModify = useCallback(
(index: number) => () => {
modifyRate(index)
},
[]
[modifyRate]
)
const handleFilterForRoom = useCallback(
@@ -193,6 +187,25 @@ export default function Rooms({
[handleFilter]
)
useEffect(() => {
setTimeout(() => {
const SCROLL_OFFSET = 100
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
const index = selectedRates.findIndex((rate) => rate === undefined)
const selectedRoom = roomElements[index - 1]
if (selectedRoom) {
const elementPosition = selectedRoom.getBoundingClientRect().top
const offsetPosition = elementPosition + window.scrollY - SCROLL_OFFSET
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
})
}
}, 0)
}, [selectedRates])
return (
<div className={styles.content}>
{isMultipleRooms ? (
@@ -232,6 +245,7 @@ export default function Rooms({
room={room}
selectedRate={rateSummary[index]}
roomCategories={roomCategories}
handleModify={handleModify(index)}
/>
</div>
<div className={styles.roomSelectionPanel}>
@@ -240,7 +254,6 @@ export default function Rooms({
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]}
setSelectedRate={setSelectedRateForRoom(index)}
hotelType={hotelType}
handleFilter={handleFilterForRoom(index)}
defaultPackages={defaultPackages}
@@ -256,7 +269,6 @@ export default function Rooms({
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[0]}
setSelectedRate={setSelectedRateForRoom(0)}
hotelType={hotelType}
handleFilter={handleFilterForRoom(0)}
defaultPackages={defaultPackages}

View File

@@ -10,10 +10,9 @@
.roomContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Large);
padding: var(--Spacing-x2) var(--Spacing-x2) 0 var(--Spacing-x2);
padding: var(--Spacing-x2);
}
.roomSelectionPanel {
@@ -24,6 +23,7 @@
opacity 0.3s ease,
grid-template-rows 0.5s ease;
height: 0;
gap: var(--Spacing-x2);
}
.roomSelectionPanel > * {
@@ -48,9 +48,11 @@
grid-template-rows: 1fr;
opacity: 1;
height: auto;
padding-bottom: var(--Spacing-x2);
}
.roomSelectionPanelContainer[data-selected="true"] .roomSelectionPanel {
display: none;
}
.roomSelectionPanelContainer[data-active-panel="true"] .roomSelectionPanel {
grid-template-rows: 1fr;
opacity: 1;

View File

@@ -1,7 +1,9 @@
"use client"
import { useIntl } from "react-intl"
import { EditIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -16,11 +18,13 @@ export default function SelectedRoomPanel({
room,
selectedRate,
roomCategories,
handleModify,
}: {
roomIndex: number
room: RoomParam
selectedRate: Rate | null
roomCategories: RoomData[]
handleModify: () => void
}) {
const intl = useIntl()
const images = roomCategories.find((roomCategory) =>
@@ -67,12 +71,25 @@ export default function SelectedRoomPanel({
})}
</Caption>
</div>
<Image
src={images?.[0]?.imageSizes?.tiny ?? ""}
alt={selectedRate?.roomType ?? images?.[0]?.metaData?.altText ?? ""}
width={240}
height={160}
/>
<div className={styles.imageAndModifyButtonContainer}>
{images && (
<div className={styles.imageContainer}>
<Image
src={images[0].imageSizes?.tiny ?? ""}
alt={selectedRate?.roomType ?? images[0].metaData?.altText ?? ""}
fill
/>
</div>
)}
<div className={styles.modifyButtonContainer}>
<Button variant="icon" size="small" onClick={handleModify}>
<EditIcon />
{intl.formatMessage({
id: "Modify",
})}
</Button>
</div>
</div>
</div>
)
}

View File

@@ -2,4 +2,35 @@
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
}
.modifyButtonContainer {
position: absolute;
right: var(--Spacing-x2);
bottom: var(--Spacing-x2);
}
.imageContainer {
width: 240px;
height: 160px;
position: relative;
}
@media (max-width: 768px) {
.imageContainer {
width: 120px;
height: 80px;
}
.imageAndModifyButtonContainer {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--Spacing-x1);
}
.modifyButtonContainer {
position: relative;
bottom: 0;
right: 0;
}
}

27
stores/rate-selection.ts Normal file
View File

@@ -0,0 +1,27 @@
import { create } from "zustand"
import type { RateCode } from "@/types/components/hotelReservation/selectRate/selectRate"
interface RateSelectionState {
selectedRates: (RateCode | undefined)[]
setSelectedRates: (rates: (RateCode | undefined)[]) => void
modifyRate: (index: number) => void
selectRate: (index: number, rate: RateCode | undefined) => void
}
export const useRateSelectionStore = create<RateSelectionState>((set) => ({
selectedRates: [],
setSelectedRates: (rates) => set({ selectedRates: rates }),
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 }
}),
}))

View File

@@ -7,7 +7,6 @@ import type {
RoomConfiguration,
} from "@/server/routers/hotels/output"
import type { RoomPackage } from "./roomFilter"
import type { RateCode } from "./selectRate"
type ProductPrice = z.output<typeof productTypePriceSchema>
export type RoomPriceSchema = z.output<typeof priceSchema>
@@ -20,12 +19,11 @@ export type FlexibilityOptionProps = {
priceInformation?: Array<string>
roomTypeCode: RoomConfiguration["roomTypeCode"]
petRoomPackage: RoomPackage | undefined
handleSelectRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
roomListIndex: number
}
export interface PriceListProps {
publicPrice?: ProductPrice | Record<string, never>
memberPrice?: ProductPrice | Record<string, never>
petRoomPackage?: RoomPackage | undefined
petRoomPackage?: RoomPackage
}

View File

@@ -8,7 +8,6 @@ import type {
} from "@/server/routers/hotels/output"
import type { RoomPriceSchema } from "./flexibilityOption"
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
import type { RateCode } from "./selectRate"
export type RoomCardProps = {
hotelId: string
@@ -18,7 +17,6 @@ export type RoomCardProps = {
roomCategories: RoomData[]
selectedPackages: RoomPackageCodes[]
packages: RoomPackageData | undefined
handleSelectRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
roomListIndex: number
}

View File

@@ -7,14 +7,12 @@ import type {
RoomPackageCodes,
RoomPackageData,
} from "./roomFilter"
import type { RateCode } from "./selectRate"
export interface RoomListProps {
roomsAvailability: RoomsAvailability
roomCategories: RoomData[]
availablePackages: RoomPackageData | undefined
selectedPackages: RoomPackageCodes[]
setRateCode: React.Dispatch<React.SetStateAction<RateCode | undefined>>
hotelType: string | undefined
roomListIndex: number
}
@@ -32,7 +30,6 @@ export interface RoomSelectionPanelProps {
roomCategories: RoomData[]
availablePackages: RoomPackage[]
selectedPackages: RoomPackageCodes[]
setSelectedRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
hotelType: string | undefined
handleFilter: (
filter: Record<RoomPackageCodeEnum, boolean | undefined>