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:
@@ -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 />
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user