feat(BOOK-54): Adjusted filter functionality to save filters as query parameters instead of paths

* feat(BOOK-54): Destination filters now matching on id instead of slug in preparation for filters from Contentstack

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-09-18 13:03:01 +00:00
parent 32a817fa72
commit 948c86479a
18 changed files with 136 additions and 102 deletions

View File

@@ -44,16 +44,24 @@ export default function HotelList() {
return
}
function handleBoundsChanged() {
debouncedUpdateVisibleHotels()
}
coreLib.event.addListener(map, "bounds_changed", handleBoundsChanged)
coreLib.event.addListener(
map,
"bounds_changed",
debouncedUpdateVisibleHotels
)
return () => {
coreLib.event.clearListeners(map, "bounds_changed")
}
}, [map, coreLib, debouncedUpdateVisibleHotels])
useEffect(() => {
if (!map) {
return
}
setVisibleHotels(getVisibleHotels(activeHotels, map))
}, [map, activeHotels])
if (isLoading) {
return <HotelListSkeleton />
}

View File

@@ -29,11 +29,11 @@ export default function CityMap({
defaultLocation,
}: CityMapProps) {
const intl = useIntl()
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
const { activeHotels, allFilters, filterFromUrl } = useDestinationDataStore(
(state) => ({
activeHotels: state.activeHotels,
allFilters: state.allFilters,
activeFilters: state.activeFilters,
filterFromUrl: state.filterFromUrl,
})
)
@@ -47,7 +47,7 @@ export default function CityMap({
>
<Typography variant="Title/sm">
<h1 className={styles.title}>
{getHeadingText(intl, city.name, allFilters, activeFilters[0])}
{getHeadingText(intl, city.name, allFilters, filterFromUrl)}
</h1>
</Typography>
<HotelList />

View File

@@ -28,7 +28,6 @@ import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import DestinationTracking from "../Tracking"
import { getHeadingText } from "../utils"
import CityMap from "./CityMap"
import DestinationCityPageSkeleton from "./DestinationCityPageSkeleton"
@@ -36,12 +35,10 @@ import styles from "./destinationCityPage.module.css"
interface DestinationCityPageProps {
isMapView: boolean
filterFromUrl?: string
}
export default async function DestinationCityPage({
isMapView,
filterFromUrl,
}: DestinationCityPageProps) {
const intl = await getIntl()
const lang = await getLang()
@@ -94,7 +91,6 @@ export default async function DestinationCityPage({
<DestinationDataProvider
allHotels={allHotels}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
pathname={pathname}
>
@@ -120,16 +116,7 @@ export default async function DestinationCityPage({
{blocks && <Blocks blocks={blocks} />}
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper
hasActiveFilter={!!filterFromUrl}
preamble={preamble}
heading={getHeadingText(
intl,
city.name,
allFilters,
filterFromUrl
)}
>
<SidebarContentWrapper preamble={preamble} location={city.name}>
<ExperienceList experiences={experiences} />
{has_sidepeek && sidepeek_content ? (
<DestinationPageSidePeek

View File

@@ -28,11 +28,11 @@ export default function CountryMap({
defaultLocation,
}: CountryMapProps) {
const intl = useIntl()
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
const { activeHotels, allFilters, filterFromUrl } = useDestinationDataStore(
(state) => ({
activeHotels: state.activeHotels,
allFilters: state.allFilters,
activeFilters: state.activeFilters,
filterFromUrl: state.filterFromUrl,
})
)
@@ -46,7 +46,7 @@ export default function CountryMap({
>
<Typography variant="Title/sm">
<h1 className={styles.title}>
{getHeadingText(intl, country, allFilters, activeFilters[0])}
{getHeadingText(intl, country, allFilters, filterFromUrl)}
</h1>
</Typography>
<CityList />

View File

@@ -28,7 +28,6 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import TopImages from "../TopImages"
import DestinationTracking from "../Tracking"
import { getHeadingText } from "../utils"
import CountryMap from "./CountryMap"
import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
@@ -36,12 +35,10 @@ import styles from "./destinationCountryPage.module.css"
interface DestinationCountryPageProps {
isMapView: boolean
filterFromUrl?: string
}
export default async function DestinationCountryPage({
isMapView,
filterFromUrl,
}: DestinationCountryPageProps) {
const intl = await getIntl()
const lang = await getLang()
@@ -93,7 +90,6 @@ export default async function DestinationCountryPage({
allHotels={allHotels}
allCities={allCities}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
pathname={pathname}
>
@@ -123,14 +119,8 @@ export default async function DestinationCountryPage({
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper
hasActiveFilter={!!filterFromUrl}
preamble={preamble}
heading={getHeadingText(
intl,
translatedCountry,
allFilters,
filterFromUrl
)}
location={translatedCountry}
>
<ExperienceList experiences={experiences} />
{has_sidepeek && sidepeek_content ? (

View File

@@ -1,37 +1,47 @@
"use client"
import { useRef } from "react"
import { useIntl } from "react-intl"
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationDataStore } from "@/stores/destination-data"
import { getHeadingText } from "@/components/ContentType/DestinationPage/utils"
import styles from "./sidebarContentWrapper.module.css"
interface SidebarContentWrapperProps extends React.PropsWithChildren {
preamble: string
hasActiveFilter: boolean
heading: string
location: string
}
export default function SidebarContentWrapper({
preamble,
hasActiveFilter,
heading,
location,
children,
}: SidebarContentWrapperProps) {
const intl = useIntl()
const sidebarRef = useRef<HTMLDivElement>(null)
const { allFilters, filterFromUrl } = useDestinationDataStore((state) => ({
allFilters: state.allFilters,
filterFromUrl: state.filterFromUrl,
}))
useStickyPosition({
ref: sidebarRef,
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
})
const heading = getHeadingText(intl, location, allFilters, filterFromUrl)
return (
<div ref={sidebarRef} className={styles.sidebarContent}>
<Typography variant="Title/md">
<h1 className={styles.heading}>{heading}</h1>
</Typography>
{!hasActiveFilter ? (
{!filterFromUrl ? (
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.text}>{preamble}</p>
</Typography>

View File

@@ -1,18 +1,21 @@
import type { CategorizedHotelFilters } from "@scandic-hotels/trpc/types/hotel"
import type {
CategorizedHotelFilters,
HotelFilter,
} from "@scandic-hotels/trpc/types/hotel"
import type { IntlShape } from "react-intl"
export function getHeadingText(
intl: IntlShape,
location: string,
allFilters: CategorizedHotelFilters,
filter?: string
filterFromUrl: HotelFilter | null
) {
if (filter) {
if (filterFromUrl) {
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.slug === filter
(f) => f.id === filterFromUrl.id
)
const surroudingsFilter = allFilters.surroundingsFilters.find(
(f) => f.slug === filter
(f) => f.id === filterFromUrl.id
)
if (facilityFilter) {

View File

@@ -53,8 +53,10 @@ export default function Filter({ filters }: FilterProps) {
<Checkbox
name={filter.name}
value={filter.slug}
onChange={() => togglePendingFilter(filter.slug)}
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
onChange={() => togglePendingFilter(filter)}
isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id)
}
/>
</li>
))}
@@ -75,8 +77,10 @@ export default function Filter({ filters }: FilterProps) {
<Checkbox
name={filter.name}
value={filter.slug}
onChange={() => togglePendingFilter(filter.slug)}
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
onChange={() => togglePendingFilter(filter)}
isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id)
}
/>
</li>
))}

View File

@@ -43,6 +43,7 @@ export default function DestinationFilterAndSort({
basePath,
pendingCount,
activeFilters,
filterFromUrl,
clearPendingFilters,
resetPendingValues,
setIsLoading,
@@ -56,6 +57,7 @@ export default function DestinationFilterAndSort({
pendingCount:
listType === "city" ? state.pendingCityCount : state.pendingHotelCount,
activeFilters: state.activeFilters,
filterFromUrl: state.filterFromUrl,
clearPendingFilters: state.actions.clearPendingFilters,
resetPendingValues: state.actions.resetPendingValues,
setIsLoading: state.actions.setIsLoading,
@@ -84,7 +86,7 @@ export default function DestinationFilterAndSort({
function submitAndClose(close: () => void) {
setIsLoading(true)
const sort = pendingSort
const filters = pendingFilters
const filters = [...pendingFilters]
const parsedUrl = new URL(window.location.href)
const searchParams = parsedUrl.searchParams
if (sort === defaultSort && searchParams.has("sort")) {
@@ -92,14 +94,26 @@ export default function DestinationFilterAndSort({
} else if (sort !== defaultSort) {
searchParams.set("sort", sort)
}
const [firstFilter, ...remainingFilters] = filters
parsedUrl.pathname = basePath
if (firstFilter) {
parsedUrl.pathname += `/${firstFilter}`
// We need to check if one of the filters in the URL is also in the pending filters.
// This is only the case when the user has navigated directly to a filter URL
// e.g. /destination/copenhagen/spa and then opens the filter modal and changes the sort or adds/removes other filters.
// In this case we will keep the filter in the URL and not add it to the "filter" search param.
if (filterFromUrl) {
const foundFilterIndex = filters.findIndex(
(f) => f.id === filterFromUrl.id
)
if (foundFilterIndex !== -1) {
parsedUrl.pathname += `/${filterFromUrl.slug}`
filters.splice(foundFilterIndex, 1)
}
}
if (remainingFilters.length > 0) {
searchParams.set("filter", remainingFilters.join("&"))
if (filters.length > 0) {
const filterSlugs = filters.map((f) => f.slug)
searchParams.set("filter", filterSlugs.join("&"))
} else {
searchParams.delete("filter")
}