feat(SW-718) created useRoomFilteringStore

This commit is contained in:
Pontus Dreij
2025-01-28 14:44:37 +01:00
parent ba20ce2696
commit ab7b826cd2
7 changed files with 144 additions and 137 deletions

View File

@@ -1,6 +1,8 @@
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useMemo } from "react" import { useMemo } from "react"
import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering"
import RoomTypeFilter from "../RoomTypeFilter" import RoomTypeFilter from "../RoomTypeFilter"
import RoomTypeList from "../RoomTypeList" import RoomTypeList from "../RoomTypeList"
@@ -8,16 +10,17 @@ import type { FilterValues } from "@/types/components/hotelReservation/selectRat
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection" import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export function RoomSelectionPanel({ export function RoomSelectionPanel({
rooms,
roomCategories, roomCategories,
availablePackages, availablePackages,
selectedPackages, selectedPackages,
hotelType, hotelType,
handleFilter,
defaultPackages, defaultPackages,
roomListIndex, roomListIndex,
}: RoomSelectionPanelProps) { }: RoomSelectionPanelProps) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { getRooms } = useRoomFilteringStore()
const rooms = getRooms(roomListIndex)
const initialFilterValues = useMemo(() => { const initialFilterValues = useMemo(() => {
const packagesFromSearchParams = const packagesFromSearchParams =
@@ -32,19 +35,21 @@ export function RoomSelectionPanel({
return ( return (
<> <>
<RoomTypeFilter <RoomTypeFilter
numberOfRooms={rooms.roomConfigurations.length} numberOfRooms={rooms?.roomConfigurations.length ?? 0}
onFilter={handleFilter}
filterOptions={defaultPackages} filterOptions={defaultPackages}
initialFilterValues={initialFilterValues} initialFilterValues={initialFilterValues}
/>
<RoomTypeList
roomsAvailability={rooms}
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackages}
hotelType={hotelType}
roomListIndex={roomListIndex} roomListIndex={roomListIndex}
/> />
{rooms && (
<RoomTypeList
roomsAvailability={rooms}
roomCategories={roomCategories}
availablePackages={availablePackages}
selectedPackages={selectedPackages}
hotelType={hotelType}
roomListIndex={roomListIndex}
/>
)}
</> </>
) )
} }

View File

@@ -7,6 +7,8 @@ import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts" import { useMediaQuery } from "usehooks-ts"
import { z } from "zod" import { z } from "zod"
import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering"
import { getIconForFeatureCode } from "@/components/HotelReservation/utils" import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
import { InfoCircleIcon } from "@/components/Icons" import { InfoCircleIcon } from "@/components/Icons"
import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox" import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox"
@@ -24,13 +26,12 @@ import {
export default function RoomFilter({ export default function RoomFilter({
numberOfRooms, numberOfRooms,
onFilter,
filterOptions, filterOptions,
initialFilterValues, initialFilterValues,
roomListIndex,
}: 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 [isInitialized, setIsInitialized] = useState(false)
const intl = useIntl() const intl = useIntl()
const methods = useForm<FilterValues>({ const methods = useForm<FilterValues>({
@@ -40,6 +41,8 @@ export default function RoomFilter({
resolver: zodResolver(z.object({})), resolver: zodResolver(z.object({})),
}) })
const { handleFilter } = useRoomFilteringStore()
const { watch, getValues } = methods const { watch, getValues } = methods
const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM) const petFriendly = watch(RoomPackageCodeEnum.PET_ROOM)
const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM) const allergyFriendly = watch(RoomPackageCodeEnum.ALLERGY_ROOM)
@@ -51,23 +54,18 @@ export default function RoomFilter({
}) })
useEffect(() => { useEffect(() => {
if (!initialFilterValues || isInitialized) return if (!initialFilterValues) return
onFilter(initialFilterValues) handleFilter(initialFilterValues, roomListIndex)
setIsInitialized(true) }, [initialFilterValues, handleFilter, roomListIndex])
}, [initialFilterValues, onFilter, isInitialized])
// Watch for filter changes // Watch for filter changes
useEffect(() => { useEffect(() => {
const subscription = watch((value, { name }) => { const subscription = watch((value, { name }) => {
if (!name || !isInitialized) return if (name) handleFilter(getValues(), roomListIndex)
const currentValues = getValues()
onFilter(currentValues)
}) })
return () => subscription.unsubscribe() return () => subscription.unsubscribe()
}, [watch, getValues, onFilter, isInitialized]) }, [watch, getValues, handleFilter, roomListIndex])
useEffect(() => { useEffect(() => {
setIsAboveMobile(isTabletAndUp) setIsAboveMobile(isTabletAndUp)

View File

@@ -1,13 +1,13 @@
"use client" "use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useRateSelectionStore } from "@/stores/select-rate/rate-selection" import { useRateSelectionStore } from "@/stores/select-rate/rate-selection"
import { useRoomFilteringStore } from "@/stores/select-rate/room-filtering"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { useRoomFiltering } from "@/hooks/selectRate/useRoomFiltering"
import { trackLowestRoomPrice } from "@/utils/tracking" import { trackLowestRoomPrice } from "@/utils/tracking"
import { convertObjToSearchParams, convertSearchParamsToObj } from "@/utils/url" import { convertObjToSearchParams, convertSearchParamsToObj } from "@/utils/url"
@@ -45,6 +45,13 @@ export default function Rooms({
const { selectedRates, rateSummary, calculateRateSummary, initializeRates } = const { selectedRates, rateSummary, calculateRateSummary, initializeRates } =
useRateSelectionStore() useRateSelectionStore()
const {
selectedPackagesByRoom,
setVisibleRooms,
setRoomsAvailability,
getFilteredRooms,
} = useRoomFilteringStore()
const bookingWidgetSearchData = useMemo( const bookingWidgetSearchData = useMemo(
() => () =>
convertSearchParamsToObj<SelectRateSearchParams>( convertSearchParamsToObj<SelectRateSearchParams>(
@@ -109,8 +116,10 @@ export default function Rooms({
[availablePackages, intl] [availablePackages, intl]
) )
const { selectedPackagesByRoom, getRooms, handleFilter, getFilteredRooms } = useEffect(() => {
useRoomFiltering({ roomsAvailability }) setRoomsAvailability(roomsAvailability)
setVisibleRooms()
}, [roomsAvailability, setRoomsAvailability, setVisibleRooms])
useEffect(() => { useEffect(() => {
if ( if (
@@ -183,14 +192,6 @@ export default function Rooms({
router.push(`select-bed?${queryParams}`) router.push(`select-bed?${queryParams}`)
} }
const handleFilterForRoom = useCallback(
(index: number) =>
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
handleFilter(filter, index)
},
[handleFilter]
)
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
const SCROLL_OFFSET = 100 const SCROLL_OFFSET = 100
@@ -252,12 +253,10 @@ export default function Rooms({
</div> </div>
<div className={styles.roomSelectionPanel}> <div className={styles.roomSelectionPanel}>
<RoomSelectionPanel <RoomSelectionPanel
rooms={getRooms(index)}
roomCategories={roomCategories} roomCategories={roomCategories}
availablePackages={availablePackages} availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]} selectedPackages={selectedPackagesByRoom[index]}
hotelType={hotelType} hotelType={hotelType}
handleFilter={handleFilterForRoom(index)}
defaultPackages={defaultPackages} defaultPackages={defaultPackages}
roomListIndex={index} roomListIndex={index}
/> />
@@ -268,12 +267,10 @@ export default function Rooms({
}) })
) : ( ) : (
<RoomSelectionPanel <RoomSelectionPanel
rooms={getRooms(0)}
roomCategories={roomCategories} roomCategories={roomCategories}
availablePackages={availablePackages} availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[0]} selectedPackages={selectedPackagesByRoom[0]}
hotelType={hotelType} hotelType={hotelType}
handleFilter={handleFilterForRoom(0)}
defaultPackages={defaultPackages} defaultPackages={defaultPackages}
roomListIndex={0} roomListIndex={0}
/> />

View File

@@ -1,92 +0,0 @@
import { useCallback, useMemo, useState } from "react"
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
import type {
RoomPackageCodeEnum,
RoomPackageCodes,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
RoomConfiguration,
RoomsAvailability,
} from "@/server/routers/hotels/output"
interface UseRoomFilteringProps {
roomsAvailability: RoomsAvailability
}
export function useRoomFiltering({ roomsAvailability }: UseRoomFilteringProps) {
const [selectedPackagesByRoom, setSelectedPackagesByRoom] = useState<
Record<number, RoomPackageCodes[]>
>({})
const visibleRooms = useMemo(() => {
const deduped = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
)
const separated = deduped.reduce<{
available: RoomConfiguration[]
notAvailable: RoomConfiguration[]
}>(
(acc, curr) => {
if (curr.status === "NotAvailable")
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
return { ...acc, available: [...acc.available, curr] }
},
{ available: [], notAvailable: [] }
)
return [...separated.available, ...separated.notAvailable]
}, [roomsAvailability.roomConfigurations])
const handleFilter = useCallback(
(
filter: Record<RoomPackageCodeEnum, boolean | undefined>,
roomIndex: number
) => {
const filteredPackages = Object.keys(filter).filter(
(key) => filter[key as RoomPackageCodeEnum]
) as RoomPackageCodeEnum[]
setSelectedPackagesByRoom((prev) => ({
...prev,
[roomIndex]: filteredPackages,
}))
},
[]
)
const getFilteredRooms = useCallback(
(roomIndex: number) => {
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
return visibleRooms.filter((room) =>
selectedPackages.every((filteredPackage) =>
room.features.some((feature) => feature.code === filteredPackage)
)
)
},
[visibleRooms, selectedPackagesByRoom]
)
const getRooms = useCallback(
(roomIndex: number) => {
const selectedPackages = selectedPackagesByRoom[roomIndex] || []
const filteredRooms = getFilteredRooms(roomIndex)
return {
...roomsAvailability,
roomConfigurations:
selectedPackages.length === 0 ? visibleRooms : filteredRooms,
}
},
[roomsAvailability, visibleRooms, selectedPackagesByRoom, getFilteredRooms]
)
return {
selectedPackagesByRoom,
getFilteredRooms,
getRooms,
handleFilter,
}
}

View File

@@ -0,0 +1,104 @@
import { create } from "zustand"
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
import type {
FilterValues,
RoomPackageCodeEnum,
RoomPackageCodes,
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
RoomConfiguration,
RoomsAvailability,
} from "@/server/routers/hotels/output"
interface RoomFilteringState {
selectedPackagesByRoom: Record<number, RoomPackageCodes[]>
roomsAvailability: RoomsAvailability | null
visibleRooms: RoomConfiguration[]
setVisibleRooms: () => void
setRoomsAvailability: (rooms: RoomsAvailability) => void
handleFilter: (filter: FilterValues, roomIndex: number) => void
getFilteredRooms: (roomIndex: number) => RoomConfiguration[]
getRooms: (roomIndex: number) => RoomsAvailability | null
}
export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
selectedPackagesByRoom: {},
roomsAvailability: null,
visibleRooms: [],
setRoomsAvailability: (rooms) => {
set({ roomsAvailability: rooms })
},
setVisibleRooms: () => {
const { roomsAvailability } = get()
if (!roomsAvailability) return null
const deduped = filterDuplicateRoomTypesByLowestPrice(
roomsAvailability.roomConfigurations
)
const separated = deduped.reduce<{
available: RoomConfiguration[]
notAvailable: RoomConfiguration[]
}>(
(acc, curr) => {
if (curr.status === "NotAvailable")
return { ...acc, notAvailable: [...acc.notAvailable, curr] }
return { ...acc, available: [...acc.available, curr] }
},
{ available: [], notAvailable: [] }
)
set({ visibleRooms: [...separated.available, ...separated.notAvailable] })
},
handleFilter: (filter, roomIndex) => {
const filteredPackages = Object.entries(filter)
.filter(([_, isSelected]) => isSelected)
.map(([code]) => code) as RoomPackageCodeEnum[]
set((state) => {
const currentPackages = state.selectedPackagesByRoom[roomIndex] || []
const sortedCurrent = [...currentPackages].sort()
const sortedNew = [...filteredPackages].sort()
if (JSON.stringify(sortedCurrent) === JSON.stringify(sortedNew)) {
return state
}
return {
...state,
selectedPackagesByRoom: {
...state.selectedPackagesByRoom,
[roomIndex]: filteredPackages,
},
}
})
},
getFilteredRooms: (roomIndex) => {
const state = get()
const selectedPackages = state.selectedPackagesByRoom[roomIndex] || []
return state.visibleRooms.filter((room) =>
selectedPackages.every((filteredPackage) =>
room.features.some((feature) => feature.code === filteredPackage)
)
)
},
getRooms: (roomIndex) => {
const state = get()
if (!state.roomsAvailability) return null
const selectedPackages = state.selectedPackagesByRoom[roomIndex] || []
const filteredRooms = state.getFilteredRooms(roomIndex)
return {
...state.roomsAvailability,
roomConfigurations:
selectedPackages.length === 0 ? state.visibleRooms : filteredRooms,
}
},
}))

View File

@@ -19,9 +19,9 @@ export interface FilterValues {
} }
export interface RoomFilterProps { export interface RoomFilterProps {
numberOfRooms: number numberOfRooms: number
onFilter: (filter: Record<string, boolean | undefined>) => void
filterOptions: DefaultFilterOptions[] filterOptions: DefaultFilterOptions[]
initialFilterValues: FilterValues initialFilterValues: FilterValues
roomListIndex: number
} }
export type RoomPackage = z.output<typeof packagesSchema> export type RoomPackage = z.output<typeof packagesSchema>

View File

@@ -3,7 +3,6 @@ import type { RoomsAvailability } from "@/server/routers/hotels/output"
import type { import type {
DefaultFilterOptions, DefaultFilterOptions,
RoomPackage, RoomPackage,
RoomPackageCodeEnum,
RoomPackageCodes, RoomPackageCodes,
RoomPackageData, RoomPackageData,
} from "./roomFilter" } from "./roomFilter"
@@ -26,14 +25,10 @@ export interface SelectRateProps {
} }
export interface RoomSelectionPanelProps { export interface RoomSelectionPanelProps {
rooms: RoomsAvailability
roomCategories: RoomData[] roomCategories: RoomData[]
availablePackages: RoomPackage[] availablePackages: RoomPackage[]
selectedPackages: RoomPackageCodes[] selectedPackages: RoomPackageCodes[]
hotelType: string | undefined hotelType: string | undefined
handleFilter: (
filter: Record<RoomPackageCodeEnum, boolean | undefined>
) => void
defaultPackages: DefaultFilterOptions[] defaultPackages: DefaultFilterOptions[]
roomListIndex: number roomListIndex: number
} }