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:
@@ -10,16 +10,13 @@ import type { PageArgs } from "@/types/params"
|
|||||||
export { generateMetadata } from "@/utils/metadata/generateMetadata"
|
export { generateMetadata } from "@/utils/metadata/generateMetadata"
|
||||||
|
|
||||||
export default async function DestinationCityPagePage(
|
export default async function DestinationCityPagePage(
|
||||||
props: PageArgs<object, { view?: "map"; filterFromUrl?: string }>
|
props: PageArgs<object, { view?: "map" }>
|
||||||
) {
|
) {
|
||||||
const searchParams = await props.searchParams
|
const searchParams = await props.searchParams
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
||||||
<DestinationCityPage
|
<DestinationCityPage isMapView={searchParams.view === "map"} />
|
||||||
isMapView={searchParams.view === "map"}
|
|
||||||
filterFromUrl={searchParams.filterFromUrl}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,20 +5,16 @@ import DestinationCountryPageSkeleton from "@/components/ContentType/Destination
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import type { PageArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export { generateMetadata } from "@/utils/metadata/generateMetadata"
|
export { generateMetadata } from "@/utils/metadata/generateMetadata"
|
||||||
|
|
||||||
export default async function DestinationCountryPagePage(
|
export default async function DestinationCountryPagePage() {
|
||||||
props: PageArgs<object, { view?: "map"; filterFromUrl?: string }>
|
// props: PageArgs<{}, { view?: "map"; }>
|
||||||
) {
|
// const searchParams = await props.searchParams
|
||||||
const searchParams = await props.searchParams
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<Suspense fallback={<DestinationCountryPageSkeleton />}>
|
<Suspense fallback={<DestinationCountryPageSkeleton />}>
|
||||||
<DestinationCountryPage
|
<DestinationCountryPage
|
||||||
// isMapView={searchParams.view === "map"} // Disabled until further notice
|
// isMapView={searchParams.view === "map"} // Disabled until further notice
|
||||||
filterFromUrl={searchParams.filterFromUrl}
|
|
||||||
isMapView={false}
|
isMapView={false}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -44,16 +44,24 @@ export default function HotelList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBoundsChanged() {
|
coreLib.event.addListener(
|
||||||
debouncedUpdateVisibleHotels()
|
map,
|
||||||
}
|
"bounds_changed",
|
||||||
|
debouncedUpdateVisibleHotels
|
||||||
coreLib.event.addListener(map, "bounds_changed", handleBoundsChanged)
|
)
|
||||||
return () => {
|
return () => {
|
||||||
coreLib.event.clearListeners(map, "bounds_changed")
|
coreLib.event.clearListeners(map, "bounds_changed")
|
||||||
}
|
}
|
||||||
}, [map, coreLib, debouncedUpdateVisibleHotels])
|
}, [map, coreLib, debouncedUpdateVisibleHotels])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisibleHotels(getVisibleHotels(activeHotels, map))
|
||||||
|
}, [map, activeHotels])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <HotelListSkeleton />
|
return <HotelListSkeleton />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export default function CityMap({
|
|||||||
defaultLocation,
|
defaultLocation,
|
||||||
}: CityMapProps) {
|
}: CityMapProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
|
const { activeHotels, allFilters, filterFromUrl } = useDestinationDataStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
activeHotels: state.activeHotels,
|
activeHotels: state.activeHotels,
|
||||||
allFilters: state.allFilters,
|
allFilters: state.allFilters,
|
||||||
activeFilters: state.activeFilters,
|
filterFromUrl: state.filterFromUrl,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export default function CityMap({
|
|||||||
>
|
>
|
||||||
<Typography variant="Title/sm">
|
<Typography variant="Title/sm">
|
||||||
<h1 className={styles.title}>
|
<h1 className={styles.title}>
|
||||||
{getHeadingText(intl, city.name, allFilters, activeFilters[0])}
|
{getHeadingText(intl, city.name, allFilters, filterFromUrl)}
|
||||||
</h1>
|
</h1>
|
||||||
</Typography>
|
</Typography>
|
||||||
<HotelList />
|
<HotelList />
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import DestinationPageSidePeek from "../Sidepeek"
|
|||||||
import StaticMap from "../StaticMap"
|
import StaticMap from "../StaticMap"
|
||||||
import TopImages from "../TopImages"
|
import TopImages from "../TopImages"
|
||||||
import DestinationTracking from "../Tracking"
|
import DestinationTracking from "../Tracking"
|
||||||
import { getHeadingText } from "../utils"
|
|
||||||
import CityMap from "./CityMap"
|
import CityMap from "./CityMap"
|
||||||
import DestinationCityPageSkeleton from "./DestinationCityPageSkeleton"
|
import DestinationCityPageSkeleton from "./DestinationCityPageSkeleton"
|
||||||
|
|
||||||
@@ -36,12 +35,10 @@ import styles from "./destinationCityPage.module.css"
|
|||||||
|
|
||||||
interface DestinationCityPageProps {
|
interface DestinationCityPageProps {
|
||||||
isMapView: boolean
|
isMapView: boolean
|
||||||
filterFromUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function DestinationCityPage({
|
export default async function DestinationCityPage({
|
||||||
isMapView,
|
isMapView,
|
||||||
filterFromUrl,
|
|
||||||
}: DestinationCityPageProps) {
|
}: DestinationCityPageProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
const lang = await getLang()
|
||||||
@@ -94,7 +91,6 @@ export default async function DestinationCityPage({
|
|||||||
<DestinationDataProvider
|
<DestinationDataProvider
|
||||||
allHotels={allHotels}
|
allHotels={allHotels}
|
||||||
allFilters={allFilters}
|
allFilters={allFilters}
|
||||||
filterFromUrl={filterFromUrl}
|
|
||||||
sortItems={sortItems}
|
sortItems={sortItems}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
>
|
>
|
||||||
@@ -120,16 +116,7 @@ export default async function DestinationCityPage({
|
|||||||
{blocks && <Blocks blocks={blocks} />}
|
{blocks && <Blocks blocks={blocks} />}
|
||||||
</main>
|
</main>
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<SidebarContentWrapper
|
<SidebarContentWrapper preamble={preamble} location={city.name}>
|
||||||
hasActiveFilter={!!filterFromUrl}
|
|
||||||
preamble={preamble}
|
|
||||||
heading={getHeadingText(
|
|
||||||
intl,
|
|
||||||
city.name,
|
|
||||||
allFilters,
|
|
||||||
filterFromUrl
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ExperienceList experiences={experiences} />
|
<ExperienceList experiences={experiences} />
|
||||||
{has_sidepeek && sidepeek_content ? (
|
{has_sidepeek && sidepeek_content ? (
|
||||||
<DestinationPageSidePeek
|
<DestinationPageSidePeek
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export default function CountryMap({
|
|||||||
defaultLocation,
|
defaultLocation,
|
||||||
}: CountryMapProps) {
|
}: CountryMapProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
|
const { activeHotels, allFilters, filterFromUrl } = useDestinationDataStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
activeHotels: state.activeHotels,
|
activeHotels: state.activeHotels,
|
||||||
allFilters: state.allFilters,
|
allFilters: state.allFilters,
|
||||||
activeFilters: state.activeFilters,
|
filterFromUrl: state.filterFromUrl,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export default function CountryMap({
|
|||||||
>
|
>
|
||||||
<Typography variant="Title/sm">
|
<Typography variant="Title/sm">
|
||||||
<h1 className={styles.title}>
|
<h1 className={styles.title}>
|
||||||
{getHeadingText(intl, country, allFilters, activeFilters[0])}
|
{getHeadingText(intl, country, allFilters, filterFromUrl)}
|
||||||
</h1>
|
</h1>
|
||||||
</Typography>
|
</Typography>
|
||||||
<CityList />
|
<CityList />
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
|
|||||||
import DestinationPageSidePeek from "../Sidepeek"
|
import DestinationPageSidePeek from "../Sidepeek"
|
||||||
import TopImages from "../TopImages"
|
import TopImages from "../TopImages"
|
||||||
import DestinationTracking from "../Tracking"
|
import DestinationTracking from "../Tracking"
|
||||||
import { getHeadingText } from "../utils"
|
|
||||||
import CountryMap from "./CountryMap"
|
import CountryMap from "./CountryMap"
|
||||||
import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
|
import DestinationCountryPageSkeleton from "./DestinationCountryPageSkeleton"
|
||||||
|
|
||||||
@@ -36,12 +35,10 @@ import styles from "./destinationCountryPage.module.css"
|
|||||||
|
|
||||||
interface DestinationCountryPageProps {
|
interface DestinationCountryPageProps {
|
||||||
isMapView: boolean
|
isMapView: boolean
|
||||||
filterFromUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function DestinationCountryPage({
|
export default async function DestinationCountryPage({
|
||||||
isMapView,
|
isMapView,
|
||||||
filterFromUrl,
|
|
||||||
}: DestinationCountryPageProps) {
|
}: DestinationCountryPageProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
const lang = await getLang()
|
||||||
@@ -93,7 +90,6 @@ export default async function DestinationCountryPage({
|
|||||||
allHotels={allHotels}
|
allHotels={allHotels}
|
||||||
allCities={allCities}
|
allCities={allCities}
|
||||||
allFilters={allFilters}
|
allFilters={allFilters}
|
||||||
filterFromUrl={filterFromUrl}
|
|
||||||
sortItems={sortItems}
|
sortItems={sortItems}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
>
|
>
|
||||||
@@ -123,14 +119,8 @@ export default async function DestinationCountryPage({
|
|||||||
</main>
|
</main>
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<SidebarContentWrapper
|
<SidebarContentWrapper
|
||||||
hasActiveFilter={!!filterFromUrl}
|
|
||||||
preamble={preamble}
|
preamble={preamble}
|
||||||
heading={getHeadingText(
|
location={translatedCountry}
|
||||||
intl,
|
|
||||||
translatedCountry,
|
|
||||||
allFilters,
|
|
||||||
filterFromUrl
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ExperienceList experiences={experiences} />
|
<ExperienceList experiences={experiences} />
|
||||||
{has_sidepeek && sidepeek_content ? (
|
{has_sidepeek && sidepeek_content ? (
|
||||||
|
|||||||
@@ -1,37 +1,47 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
||||||
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
|
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
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"
|
import styles from "./sidebarContentWrapper.module.css"
|
||||||
|
|
||||||
interface SidebarContentWrapperProps extends React.PropsWithChildren {
|
interface SidebarContentWrapperProps extends React.PropsWithChildren {
|
||||||
preamble: string
|
preamble: string
|
||||||
hasActiveFilter: boolean
|
location: string
|
||||||
heading: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SidebarContentWrapper({
|
export default function SidebarContentWrapper({
|
||||||
preamble,
|
preamble,
|
||||||
hasActiveFilter,
|
location,
|
||||||
heading,
|
|
||||||
children,
|
children,
|
||||||
}: SidebarContentWrapperProps) {
|
}: SidebarContentWrapperProps) {
|
||||||
|
const intl = useIntl()
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null)
|
const sidebarRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { allFilters, filterFromUrl } = useDestinationDataStore((state) => ({
|
||||||
|
allFilters: state.allFilters,
|
||||||
|
filterFromUrl: state.filterFromUrl,
|
||||||
|
}))
|
||||||
useStickyPosition({
|
useStickyPosition({
|
||||||
ref: sidebarRef,
|
ref: sidebarRef,
|
||||||
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
|
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const heading = getHeadingText(intl, location, allFilters, filterFromUrl)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={sidebarRef} className={styles.sidebarContent}>
|
<div ref={sidebarRef} className={styles.sidebarContent}>
|
||||||
<Typography variant="Title/md">
|
<Typography variant="Title/md">
|
||||||
<h1 className={styles.heading}>{heading}</h1>
|
<h1 className={styles.heading}>{heading}</h1>
|
||||||
</Typography>
|
</Typography>
|
||||||
{!hasActiveFilter ? (
|
{!filterFromUrl ? (
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<p className={styles.text}>{preamble}</p>
|
<p className={styles.text}>{preamble}</p>
|
||||||
</Typography>
|
</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"
|
import type { IntlShape } from "react-intl"
|
||||||
|
|
||||||
export function getHeadingText(
|
export function getHeadingText(
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
location: string,
|
location: string,
|
||||||
allFilters: CategorizedHotelFilters,
|
allFilters: CategorizedHotelFilters,
|
||||||
filter?: string
|
filterFromUrl: HotelFilter | null
|
||||||
) {
|
) {
|
||||||
if (filter) {
|
if (filterFromUrl) {
|
||||||
const facilityFilter = allFilters.facilityFilters.find(
|
const facilityFilter = allFilters.facilityFilters.find(
|
||||||
(f) => f.slug === filter
|
(f) => f.id === filterFromUrl.id
|
||||||
)
|
)
|
||||||
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
const surroudingsFilter = allFilters.surroundingsFilters.find(
|
||||||
(f) => f.slug === filter
|
(f) => f.id === filterFromUrl.id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (facilityFilter) {
|
if (facilityFilter) {
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={filter.name}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
onChange={() => togglePendingFilter(filter.slug)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
isSelected={
|
||||||
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -75,8 +77,10 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={filter.name}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
onChange={() => togglePendingFilter(filter.slug)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
isSelected={
|
||||||
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default function DestinationFilterAndSort({
|
|||||||
basePath,
|
basePath,
|
||||||
pendingCount,
|
pendingCount,
|
||||||
activeFilters,
|
activeFilters,
|
||||||
|
filterFromUrl,
|
||||||
clearPendingFilters,
|
clearPendingFilters,
|
||||||
resetPendingValues,
|
resetPendingValues,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
@@ -56,6 +57,7 @@ export default function DestinationFilterAndSort({
|
|||||||
pendingCount:
|
pendingCount:
|
||||||
listType === "city" ? state.pendingCityCount : state.pendingHotelCount,
|
listType === "city" ? state.pendingCityCount : state.pendingHotelCount,
|
||||||
activeFilters: state.activeFilters,
|
activeFilters: state.activeFilters,
|
||||||
|
filterFromUrl: state.filterFromUrl,
|
||||||
clearPendingFilters: state.actions.clearPendingFilters,
|
clearPendingFilters: state.actions.clearPendingFilters,
|
||||||
resetPendingValues: state.actions.resetPendingValues,
|
resetPendingValues: state.actions.resetPendingValues,
|
||||||
setIsLoading: state.actions.setIsLoading,
|
setIsLoading: state.actions.setIsLoading,
|
||||||
@@ -84,7 +86,7 @@ export default function DestinationFilterAndSort({
|
|||||||
function submitAndClose(close: () => void) {
|
function submitAndClose(close: () => void) {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const sort = pendingSort
|
const sort = pendingSort
|
||||||
const filters = pendingFilters
|
const filters = [...pendingFilters]
|
||||||
const parsedUrl = new URL(window.location.href)
|
const parsedUrl = new URL(window.location.href)
|
||||||
const searchParams = parsedUrl.searchParams
|
const searchParams = parsedUrl.searchParams
|
||||||
if (sort === defaultSort && searchParams.has("sort")) {
|
if (sort === defaultSort && searchParams.has("sort")) {
|
||||||
@@ -92,14 +94,26 @@ export default function DestinationFilterAndSort({
|
|||||||
} else if (sort !== defaultSort) {
|
} else if (sort !== defaultSort) {
|
||||||
searchParams.set("sort", sort)
|
searchParams.set("sort", sort)
|
||||||
}
|
}
|
||||||
const [firstFilter, ...remainingFilters] = filters
|
|
||||||
|
|
||||||
parsedUrl.pathname = basePath
|
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 {
|
} else {
|
||||||
searchParams.delete("filter")
|
searchParams.delete("filter")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ export default function DestinationDataProviderContent({
|
|||||||
const filterParam = searchParams.get("filter")
|
const filterParam = searchParams.get("filter")
|
||||||
const filters = []
|
const filters = []
|
||||||
const pathParts = currentPathname.split("/")
|
const pathParts = currentPathname.split("/")
|
||||||
const lastPathPart = pathParts[pathParts.length - 1]
|
const filterFromUrl = pathParts[pathParts.length - 1]
|
||||||
|
|
||||||
|
// Even though the user filter is stored in the query param,
|
||||||
|
// we also support having it as the last part of the pathname
|
||||||
|
// e.g. /destination/copenhagen/spa
|
||||||
|
// In this case we need to add it to the filters array
|
||||||
if (basePath !== currentPathname) {
|
if (basePath !== currentPathname) {
|
||||||
filters.push(lastPathPart)
|
filters.push(filterFromUrl)
|
||||||
}
|
}
|
||||||
if (filterParam) {
|
if (filterParam) {
|
||||||
filters.push(...filterParam.split("&"))
|
filters.push(...filterParam.split("&"))
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default function DestinationDataProvider({
|
|||||||
allCities = [],
|
allCities = [],
|
||||||
allHotels,
|
allHotels,
|
||||||
allFilters,
|
allFilters,
|
||||||
filterFromUrl,
|
|
||||||
sortItems,
|
sortItems,
|
||||||
pathname,
|
pathname,
|
||||||
children,
|
children,
|
||||||
@@ -28,7 +27,6 @@ export default function DestinationDataProvider({
|
|||||||
allCities,
|
allCities,
|
||||||
allHotels,
|
allHotels,
|
||||||
allFilters,
|
allFilters,
|
||||||
filterFromUrl,
|
|
||||||
pathname,
|
pathname,
|
||||||
sortItems,
|
sortItems,
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type HotelFilter,
|
||||||
type HotelListingHotelData,
|
type HotelListingHotelData,
|
||||||
type HotelSortItem,
|
type HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
@@ -25,12 +26,12 @@ const HOTEL_SORTING_STRATEGIES: Partial<
|
|||||||
|
|
||||||
export function getFilteredHotels(
|
export function getFilteredHotels(
|
||||||
hotels: HotelListingHotelData[],
|
hotels: HotelListingHotelData[],
|
||||||
filters: string[]
|
filters: HotelFilter[]
|
||||||
) {
|
) {
|
||||||
if (filters.length) {
|
if (filters.length) {
|
||||||
return hotels.filter(({ hotel }) =>
|
return hotels.filter(({ hotel }) =>
|
||||||
filters.every((filter) =>
|
filters.every((filter) =>
|
||||||
hotel.detailedFacilities.some((facility) => facility.slug === filter)
|
hotel.detailedFacilities.some((facility) => facility.id === filter.id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,26 +29,30 @@ export function createDestinationDataStore({
|
|||||||
allCities,
|
allCities,
|
||||||
allHotels,
|
allHotels,
|
||||||
allFilters,
|
allFilters,
|
||||||
filterFromUrl,
|
|
||||||
pathname,
|
pathname,
|
||||||
sortItems,
|
sortItems,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: InitialState) {
|
}: InitialState) {
|
||||||
const defaultSort =
|
const defaultSort =
|
||||||
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
||||||
const allFilterSlugs = Object.values(allFilters).flatMap(
|
const flattenedFilters = Object.values(allFilters).flat<HotelFilter[]>()
|
||||||
(filter: HotelFilter[]) => filter.map((f) => f.slug)
|
const allFilterSlugs = flattenedFilters.map((filter) => filter.slug)
|
||||||
)
|
const activeFilters: HotelFilter[] = []
|
||||||
|
let filterFromUrl: HotelFilter | null = null
|
||||||
|
|
||||||
const activeFilters: string[] = filterFromUrl ? [filterFromUrl] : []
|
|
||||||
const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
|
const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
|
||||||
pathname,
|
pathname,
|
||||||
allFilterSlugs
|
allFilterSlugs
|
||||||
)
|
)
|
||||||
if (basePathnameWithoutFilters !== pathname) {
|
if (basePathnameWithoutFilters !== pathname) {
|
||||||
const pathParts = pathname.split("/")
|
const pathParts = pathname.split("/")
|
||||||
const lastPathPart = pathParts[pathParts.length - 1]
|
filterFromUrl =
|
||||||
activeFilters.push(lastPathPart)
|
flattenedFilters.find(
|
||||||
|
(filter) => filter.slug === pathParts[pathParts.length - 1]
|
||||||
|
) ?? null
|
||||||
|
if (filterFromUrl) {
|
||||||
|
activeFilters.push(filterFromUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeSort = defaultSort
|
let activeSort = defaultSort
|
||||||
@@ -65,13 +69,16 @@ export function createDestinationDataStore({
|
|||||||
|
|
||||||
return create<DestinationDataState>((set) => ({
|
return create<DestinationDataState>((set) => ({
|
||||||
actions: {
|
actions: {
|
||||||
updateActiveFiltersAndSort(filters, sort) {
|
updateActiveFiltersAndSort(filterSlugs, sort) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DestinationDataState) => {
|
produce((state: DestinationDataState) => {
|
||||||
const newSort =
|
const newSort =
|
||||||
sort && isValidSortOption(sort, state.sortItems)
|
sort && isValidSortOption(sort, state.sortItems)
|
||||||
? sort
|
? sort
|
||||||
: state.defaultSort
|
: state.defaultSort
|
||||||
|
const filters = flattenedFilters.filter((filter) =>
|
||||||
|
filterSlugs.includes(filter.slug)
|
||||||
|
)
|
||||||
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
||||||
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
||||||
const filteredCities = state.allHotels.length
|
const filteredCities = state.allHotels.length
|
||||||
@@ -86,16 +93,22 @@ export function createDestinationDataStore({
|
|||||||
if (
|
if (
|
||||||
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
|
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
|
||||||
) {
|
) {
|
||||||
const facilityFiltersUsed = filters.filter((f) =>
|
const facilityFiltersUsed = filters
|
||||||
state.allFilters.facilityFilters
|
.filter(
|
||||||
.map((ff) => ff.slug)
|
(f) =>
|
||||||
.includes(f)
|
!!state.allFilters.facilityFilters.find(
|
||||||
)
|
(ff) => ff.id === f.id
|
||||||
const surroundingsFiltersUsed = filters.filter((f) =>
|
)
|
||||||
state.allFilters.surroundingsFilters
|
)
|
||||||
.map((sf) => sf.slug)
|
.map((f) => f.slug)
|
||||||
.includes(f)
|
const surroundingsFiltersUsed = filters
|
||||||
)
|
.filter(
|
||||||
|
(f) =>
|
||||||
|
!!state.allFilters.surroundingsFilters.find(
|
||||||
|
(sf) => sf.id === f.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((f) => f.slug)
|
||||||
|
|
||||||
trackFilterChangeEvent(
|
trackFilterChangeEvent(
|
||||||
facilityFiltersUsed,
|
facilityFiltersUsed,
|
||||||
@@ -133,9 +146,12 @@ export function createDestinationDataStore({
|
|||||||
togglePendingFilter(filter) {
|
togglePendingFilter(filter) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DestinationDataState) => {
|
produce((state: DestinationDataState) => {
|
||||||
const isActive = state.pendingFilters.includes(filter)
|
const filterId = filter.id
|
||||||
|
const isActive = !!state.pendingFilters.find(
|
||||||
|
(pf) => pf.id === filterId
|
||||||
|
)
|
||||||
const filters = isActive
|
const filters = isActive
|
||||||
? state.pendingFilters.filter((f) => f !== filter)
|
? state.pendingFilters.filter((f) => f.id !== filterId)
|
||||||
: [...state.pendingFilters, filter]
|
: [...state.pendingFilters, filter]
|
||||||
const pendingHotels = getFilteredHotels(state.allHotels, filters)
|
const pendingHotels = getFilteredHotels(state.allHotels, filters)
|
||||||
const pendingCities = state.allHotels.length
|
const pendingCities = state.allHotels.length
|
||||||
@@ -180,7 +196,7 @@ export function createDestinationDataStore({
|
|||||||
activeFilters,
|
activeFilters,
|
||||||
pendingFilters: activeFilters,
|
pendingFilters: activeFilters,
|
||||||
allFilters,
|
allFilters,
|
||||||
allFilterSlugs,
|
filterFromUrl,
|
||||||
basePathnameWithoutFilters,
|
basePathnameWithoutFilters,
|
||||||
sortItems,
|
sortItems,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
||||||
import type {
|
import type {
|
||||||
CategorizedHotelFilters,
|
CategorizedHotelFilters,
|
||||||
|
HotelFilter,
|
||||||
HotelListingHotelData,
|
HotelListingHotelData,
|
||||||
HotelSortItem,
|
HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
@@ -8,9 +9,12 @@ import type {
|
|||||||
import type { ReadonlyURLSearchParams } from "next/navigation"
|
import type { ReadonlyURLSearchParams } from "next/navigation"
|
||||||
|
|
||||||
interface Actions {
|
interface Actions {
|
||||||
updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void
|
updateActiveFiltersAndSort: (
|
||||||
|
filterSlugs: string[],
|
||||||
|
sort: string | null
|
||||||
|
) => void
|
||||||
setPendingSort: (sort: HotelSortOption) => void
|
setPendingSort: (sort: HotelSortOption) => void
|
||||||
togglePendingFilter: (filter: string) => void
|
togglePendingFilter: (filter: HotelFilter) => void
|
||||||
clearPendingFilters: () => void
|
clearPendingFilters: () => void
|
||||||
resetPendingValues: () => void
|
resetPendingValues: () => void
|
||||||
setIsLoading: (isLoading: boolean) => void
|
setIsLoading: (isLoading: boolean) => void
|
||||||
@@ -31,12 +35,12 @@ export interface DestinationDataState {
|
|||||||
pendingSort: HotelSortOption
|
pendingSort: HotelSortOption
|
||||||
activeSort: HotelSortOption
|
activeSort: HotelSortOption
|
||||||
defaultSort: HotelSortOption
|
defaultSort: HotelSortOption
|
||||||
pendingFilters: string[]
|
pendingFilters: HotelFilter[]
|
||||||
activeFilters: string[]
|
activeFilters: HotelFilter[]
|
||||||
|
filterFromUrl: HotelFilter | null
|
||||||
pendingHotelCount: number
|
pendingHotelCount: number
|
||||||
pendingCityCount: number
|
pendingCityCount: number
|
||||||
allFilters: CategorizedHotelFilters
|
allFilters: CategorizedHotelFilters
|
||||||
allFilterSlugs: string[]
|
|
||||||
basePathnameWithoutFilters: string
|
basePathnameWithoutFilters: string
|
||||||
sortItems: HotelSortItem[]
|
sortItems: HotelSortItem[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
@@ -48,6 +52,5 @@ export interface InitialState
|
|||||||
"allHotels" | "allCities" | "sortItems" | "allFilters"
|
"allHotels" | "allCities" | "sortItems" | "allFilters"
|
||||||
> {
|
> {
|
||||||
pathname: string
|
pathname: string
|
||||||
filterFromUrl?: string
|
|
||||||
searchParams: ReadonlyURLSearchParams
|
searchParams: ReadonlyURLSearchParams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -87,6 +88,7 @@ export type HotelInput = z.input<typeof hotelInputSchema>
|
|||||||
export type RoomType = Pick<Room, "roomTypes" | "name">
|
export type RoomType = Pick<Room, "roomTypes" | "name">
|
||||||
|
|
||||||
export interface HotelFilter {
|
export interface HotelFilter {
|
||||||
|
id: FacilityEnum
|
||||||
name: string
|
name: string
|
||||||
slug: string
|
slug: string
|
||||||
filterType: string
|
filterType: string
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function getFiltersFromHotels(
|
|||||||
)
|
)
|
||||||
return filter
|
return filter
|
||||||
? {
|
? {
|
||||||
|
id: filter.id,
|
||||||
name: filter.name,
|
name: filter.name,
|
||||||
slug: filter.slug,
|
slug: filter.slug,
|
||||||
filterType: filter.filter,
|
filterType: filter.filter,
|
||||||
|
|||||||
Reference in New Issue
Block a user