Merged in fix/book-674-select-hotel-infinite-loop (pull request #3351)

fix(BOOK-674): Refactor how we handle hotel filters

* Refactor hotel filters store to URL state

* Rename hotel filter store


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-12-15 13:58:00 +00:00
parent 494bf2ba78
commit 713ca6562e
10 changed files with 120 additions and 139 deletions

View File

@@ -20,11 +20,11 @@ import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import useInitializeFiltersFromUrl from "../../../../hooks/useInitializeFiltersFromUrl"
import { SortOrder } from "../../../../misc/sortOrder"
import { useHotelFilterStore } from "../../../../stores/hotel-filters"
import { useHotelResultCountStore } from "../../../../stores/hotel-result-count"
import { DEFAULT_SORT } from "../../HotelSorter"
import FilterContent from "../FilterContent"
import { useHotelFilters } from "../useHotelFilters"
import styles from "./filterAndSortModal.module.css"
@@ -46,17 +46,16 @@ export default function FilterAndSortModal({
}: FilterAndSortModalProps) {
const intl = useIntl()
useInitializeFiltersFromUrl()
const searchParams = useSearchParams()
const pathname = usePathname()
const { resultCount, setFilters, activeFilters, unfilteredResultCount } =
useHotelFilterStore((state) => ({
const { resultCount, unfilteredResultCount } = useHotelResultCountStore(
(state) => ({
resultCount: state.resultCount,
setFilters: state.setFilters,
activeFilters: state.activeFilters,
unfilteredResultCount: state.unfilteredResultCount,
}))
})
)
const [activeFilters, { setFilters }] = useHotelFilters(filters)
const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT)
@@ -116,14 +115,6 @@ export default function FilterAndSortModal({
}
const newSearchParams = new URLSearchParams(searchParams)
const values = selectedFilters.join(",")
if (values === "") {
newSearchParams.delete("filters")
} else {
newSearchParams.set("filters", values)
}
newSearchParams.set("sort", sort)
window.history.replaceState(

View File

@@ -1,13 +1,7 @@
"use client"
import { usePathname, useSearchParams } from "next/navigation"
import { useCallback, useEffect } from "react"
import { trackEvent } from "@scandic-hotels/tracking/base"
import useInitializeFiltersFromUrl from "../../../../hooks/useInitializeFiltersFromUrl"
import { useHotelFilterStore } from "../../../../stores/hotel-filters"
import FilterContent from "../FilterContent"
import { useHotelFilters } from "../useHotelFilters"
import type { CategorizedHotelFilters } from "../../../../types"
@@ -17,63 +11,7 @@ type HotelFiltersProps = {
}
export default function HotelFilter({ className, filters }: HotelFiltersProps) {
const searchParams = useSearchParams()
const pathname = usePathname()
useInitializeFiltersFromUrl()
const { toggleFilter, activeFilters } = useHotelFilterStore((state) => ({
toggleFilter: state.toggleFilter,
activeFilters: state.activeFilters,
}))
const trackFiltersEvent = useCallback(() => {
const facilityMap = new Map(
filters.facilityFilters.map((f) => [f.id.toString(), f.name])
)
const surroundingsMap = new Map(
filters.surroundingsFilters.map((f) => [f.id.toString(), f.name])
)
const hotelFacilitiesFilter = activeFilters
.filter((id) => facilityMap.has(id))
.map((id) => facilityMap.get(id))
.join(",")
const hotelSurroundingsFilter = activeFilters
.filter((id) => surroundingsMap.has(id))
.map((id) => surroundingsMap.get(id))
.join(",")
trackEvent({
event: "filterUsed",
filter: {
filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`,
},
})
}, [activeFilters, filters.facilityFilters, filters.surroundingsFilters])
// Update the URL when the filters changes
useEffect(() => {
const newSearchParams = new URLSearchParams(searchParams)
const values = activeFilters.join(",")
if (values === "") {
newSearchParams.delete("filters")
} else {
newSearchParams.set("filters", values)
}
if (values !== searchParams.get("filters")) {
if (values) {
trackFiltersEvent()
}
window.history.replaceState(
null,
"",
`${pathname}?${newSearchParams.toString()}`
)
}
}, [activeFilters, pathname, searchParams, trackFiltersEvent])
const [activeFilters, { toggleFilter }] = useHotelFilters(filters)
return (
<FilterContent

View File

@@ -0,0 +1,76 @@
import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"
import { logger } from "@scandic-hotels/common/logger"
import { trackEvent } from "@scandic-hotels/tracking/base"
import type { CategorizedHotelFilters } from "../../../types"
export function useHotelFilters(filters: CategorizedHotelFilters | null) {
const [filterIds, setFilterIds] = useQueryState(
"filters",
parseAsArrayOf(parseAsString).withOptions({
history: "replace",
})
)
const update = (filterIds: string[]) => {
if (!filters) {
// This handles a usage behavior where filterIds are read but never set, in which case we don't pass filters
// to this hook. While filters are only used for tracking purposes, this prevents us from incorrectly
// updating filterIds without providing the filters and hence getting the correct tracking data.
logger.warn(
"Updating filters are not supported when filters parameter is null in useHotelFilters hook"
)
return
}
setFilterIds(filterIds.length > 0 ? filterIds : null)
trackFiltersEvent({
activeFilterIds: filterIds,
filters,
})
}
const toggleFilter = (filterId: string) => {
const newFiltersIds = filterIds?.includes(filterId)
? filterIds.filter((id) => id !== filterId)
: [...(filterIds || []), filterId]
update(newFiltersIds)
}
const activeFilters = filterIds || []
return [activeFilters, { toggleFilter, setFilters: update }] as const
}
const trackFiltersEvent = ({
activeFilterIds,
filters,
}: {
activeFilterIds: string[]
filters: CategorizedHotelFilters
}) => {
const facilityMap = new Map(
filters.facilityFilters.map((f) => [f.id.toString(), f.name])
)
const surroundingsMap = new Map(
filters.surroundingsFilters.map((f) => [f.id.toString(), f.name])
)
const hotelFacilitiesFilter = activeFilterIds
.filter((id) => facilityMap.has(id))
.map((id) => facilityMap.get(id))
.join(",")
const hotelSurroundingsFilter = activeFilterIds
.filter((id) => surroundingsMap.has(id))
.map((id) => surroundingsMap.get(id))
.join(",")
trackEvent({
event: "filterUsed",
filter: {
filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`,
},
})
}

View File

@@ -4,11 +4,11 @@ import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useHotelFilterStore } from "../../../stores/hotel-filters"
import { useHotelResultCountStore } from "../../../stores/hotel-result-count"
export default function HotelCount() {
const intl = useIntl()
const resultCount = useHotelFilterStore((state) => state.resultCount)
const resultCount = useHotelResultCountStore((state) => state.resultCount)
return (
<Typography variant="Title/Subtitle/md">

View File

@@ -7,7 +7,8 @@ import { alternativeHotels } from "@scandic-hotels/common/constants/routes/hotel
import { Alert } from "@scandic-hotels/design-system/Alert"
import useLang from "../../hooks/useLang"
import { useHotelFilterStore } from "../../stores/hotel-filters"
import { useHotelResultCountStore } from "../../stores/hotel-result-count"
import { useHotelFilters } from "./Filters/useHotelFilters"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
@@ -31,10 +32,10 @@ export default function NoAvailabilityAlert({
const intl = useIntl()
const lang = useLang()
const { resultCount, activeFilters } = useHotelFilterStore((state) => ({
const { resultCount } = useHotelResultCountStore((state) => ({
resultCount: state.resultCount,
activeFilters: state.activeFilters,
}))
const [activeFilters] = useHotelFilters(null)
if (activeFilters.length > 0 && resultCount === 0) {
return (

View File

@@ -27,12 +27,13 @@ import {
BookingCodeFilterEnum,
useBookingCodeFilterStore,
} from "../../../../stores/bookingCode-filter"
import { useHotelFilterStore } from "../../../../stores/hotel-filters"
import { useHotelResultCountStore } from "../../../../stores/hotel-result-count"
import { useHotelsMapStore } from "../../../../stores/hotels-map"
import BookingCodeFilter from "../../../BookingCodeFilter"
import { getHotelPins } from "../../../HotelCardDialogListing/utils"
import { RoomCardSkeleton } from "../../../RoomCardSkeleton/RoomCardSkeleton"
import FilterAndSortModal from "../../Filters/FilterAndSortModal"
import { useHotelFilters } from "../../Filters/useHotelFilters"
import { type HotelResponse } from "../../helpers"
import HotelListing from "../HotelListing"
import { getVisibleHotels } from "./utils"
@@ -75,8 +76,10 @@ export function SelectHotelMapContent({
const [showSkeleton, setShowSkeleton] = useState<boolean>(true)
const listingContainerRef = useRef<HTMLDivElement | null>(null)
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
const [activeFilters] = useHotelFilters(null)
const setResultCount = useHotelResultCountStore(
(state) => state.setResultCount
)
const pointsCurrency = useGetPointsCurrency()
const hotelMapStore = useHotelsMapStore()