fix(SW-2933): Making the hotels/city listing render correctly with active filter on page load

Approved-by: Christian Andolf
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-06-18 08:42:16 +00:00
parent bb98c2652e
commit 1e039febaf
12 changed files with 149 additions and 151 deletions

View File

@@ -11,7 +11,7 @@ import type { PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata" export { generateMetadata } from "@/utils/generateMetadata"
export default async function DestinationCityPagePage( export default async function DestinationCityPagePage(
props: PageArgs<{}, { view?: "map" }> props: PageArgs<{}, { view?: "map"; filterFromUrl?: string }>
) { ) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
@@ -20,7 +20,10 @@ export default async function DestinationCityPagePage(
return ( return (
<Suspense fallback={<DestinationCityPageSkeleton />}> <Suspense fallback={<DestinationCityPageSkeleton />}>
<DestinationCityPage isMapView={searchParams.view === "map"} /> <DestinationCityPage
isMapView={searchParams.view === "map"}
filterFromUrl={searchParams.filterFromUrl}
/>
</Suspense> </Suspense>
) )
} }

View File

@@ -10,10 +10,10 @@ import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata" export { generateMetadata } from "@/utils/generateMetadata"
export default function DestinationCountryPagePage({}: PageArgs< export default async function DestinationCountryPagePage(
LangParams, props: PageArgs<LangParams, { view?: "map"; filterFromUrl?: string }>
{ view?: "map" } ) {
>) { const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") { if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return notFound() return notFound()
} }
@@ -22,6 +22,7 @@ export default function DestinationCountryPagePage({}: PageArgs<
<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>

View File

@@ -1,57 +0,0 @@
import {
getDestinationCityPagesByCountry,
getHotelsByCountry,
} from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import type { Country } from "@/types/enums/country"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
interface CityDataContainerProps extends React.PropsWithChildren {
country: Country
}
export function preload(country: Country) {
void getHotelsByCountry(country)
void getDestinationCityPagesByCountry(country)
}
export default async function CityDataContainer({
country,
children,
}: CityDataContainerProps) {
const intl = await getIntl()
const [hotels, cities] = await Promise.all([
getHotelsByCountry(country),
getDestinationCityPagesByCountry(country),
])
const sortItems: SortItem[] = [
{
label: intl.formatMessage({
defaultMessage: "Recommended",
}),
value: SortOption.Recommended,
isDefault: true,
},
{
label: intl.formatMessage({
defaultMessage: "Name",
}),
value: SortOption.Name,
},
]
return (
<DestinationDataProvider
allHotels={hotels}
allCities={cities}
sortItems={sortItems}
>
{children}
</DestinationDataProvider>
)
}

View File

@@ -2,32 +2,44 @@ import { notFound } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { env } from "@/env/server" import { env } from "@/env/server"
import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests" import {
getDestinationCityPage,
getHotelsByCityIdentifier,
} from "@/lib/trpc/memoizedRequests"
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
import Breadcrumbs from "@/components/Breadcrumbs" import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import Blocks from "../Blocks" import Blocks from "../Blocks"
import ExperienceList from "../ExperienceList" import ExperienceList from "../ExperienceList"
import HotelDataContainer, { preload } from "../HotelDataContainer"
import HotelListing from "../HotelListing" import HotelListing from "../HotelListing"
import SidebarContentWrapper from "../SidebarContentWrapper" import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek" 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"
import styles from "./destinationCityPage.module.css" import styles from "./destinationCityPage.module.css"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
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 pageData = await getDestinationCityPage() const pageData = await getDestinationCityPage()
if (!pageData) { if (!pageData) {
@@ -46,12 +58,39 @@ export default async function DestinationCityPage({
destination_settings, destination_settings,
} = destinationCityPage } = destinationCityPage
preload(cityIdentifier) const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
const allFilters = getFiltersFromHotels(allHotels)
const sortItems: SortItem[] = [
{
label: intl.formatMessage({
defaultMessage: "Distance to city center",
}),
value: SortOption.Distance,
isDefault: true,
},
{
label: intl.formatMessage({
defaultMessage: "Name",
}),
value: SortOption.Name,
},
{
label: intl.formatMessage({
defaultMessage: "TripAdvisor rating",
}),
value: SortOption.TripAdvisorRating,
},
]
return ( return (
<> <>
<Suspense fallback={<DestinationCityPageSkeleton />}> <Suspense fallback={<DestinationCityPageSkeleton />}>
<HotelDataContainer cityIdentifier={cityIdentifier}> <DestinationDataProvider
allHotels={allHotels}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
>
{isMapView ? ( {isMapView ? (
<CityMap <CityMap
mapId={env.GOOGLE_DYNAMIC_MAP_ID} mapId={env.GOOGLE_DYNAMIC_MAP_ID}
@@ -74,15 +113,24 @@ export default async function DestinationCityPage({
{blocks && <Blocks blocks={blocks} />} {blocks && <Blocks blocks={blocks} />}
</main> </main>
<aside className={styles.sidebar}> <aside className={styles.sidebar}>
<SidebarContentWrapper location={city.name} preamble={preamble}> <SidebarContentWrapper
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
buttonText={sidepeek_button_text} buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content} sidePeekContent={sidepeek_content}
location={city.name} location={city.name}
/> />
)} ) : null}
{destination_settings.city && ( {destination_settings.city && (
<StaticMap <StaticMap
@@ -94,7 +142,7 @@ export default async function DestinationCityPage({
</aside> </aside>
</div> </div>
)} )}
</HotelDataContainer> </DestinationDataProvider>
</Suspense> </Suspense>
<DestinationTracking pageData={tracking} /> <DestinationTracking pageData={tracking} />
</> </>

View File

@@ -2,31 +2,44 @@ import { notFound } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { env } from "@/env/server" import { env } from "@/env/server"
import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests" import {
getDestinationCityPagesByCountry,
getDestinationCountryPage,
getHotelsByCountry,
} from "@/lib/trpc/memoizedRequests"
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
import Breadcrumbs from "@/components/Breadcrumbs" import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import Blocks from "../Blocks" import Blocks from "../Blocks"
import CityDataContainer, { preload } from "../CityDataContainer"
import CityListing from "../CityListing" import CityListing from "../CityListing"
import ExperienceList from "../ExperienceList" import ExperienceList from "../ExperienceList"
import SidebarContentWrapper from "../SidebarContentWrapper" 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"
import styles from "./destinationCountryPage.module.css" import styles from "./destinationCountryPage.module.css"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
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 pageData = await getDestinationCountryPage() const pageData = await getDestinationCountryPage()
if (!pageData) { if (!pageData) {
@@ -45,12 +58,38 @@ export default async function DestinationCountryPage({
destination_settings, destination_settings,
} = destinationCountryPage } = destinationCountryPage
preload(destination_settings.country) const [allHotels, allCities] = await Promise.all([
getHotelsByCountry(destination_settings.country),
getDestinationCityPagesByCountry(destination_settings.country),
])
const allFilters = getFiltersFromHotels(allHotels)
const sortItems: SortItem[] = [
{
label: intl.formatMessage({
defaultMessage: "Recommended",
}),
value: SortOption.Recommended,
isDefault: true,
},
{
label: intl.formatMessage({
defaultMessage: "Name",
}),
value: SortOption.Name,
},
]
return ( return (
<> <>
<Suspense fallback={<DestinationCountryPageSkeleton />}> <Suspense fallback={<DestinationCountryPageSkeleton />}>
<CityDataContainer country={destination_settings.country}> <DestinationDataProvider
allHotels={allHotels}
allCities={allCities}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
>
{isMapView ? ( {isMapView ? (
<CountryMap <CountryMap
mapId={env.GOOGLE_DYNAMIC_MAP_ID} mapId={env.GOOGLE_DYNAMIC_MAP_ID}
@@ -77,22 +116,28 @@ export default async function DestinationCountryPage({
</main> </main>
<aside className={styles.sidebar}> <aside className={styles.sidebar}>
<SidebarContentWrapper <SidebarContentWrapper
location={translatedCountry} hasActiveFilter={!!filterFromUrl}
preamble={preamble} preamble={preamble}
heading={getHeadingText(
intl,
translatedCountry,
allFilters,
filterFromUrl
)}
> >
<ExperienceList experiences={experiences} /> <ExperienceList experiences={experiences} />
{has_sidepeek && sidepeek_content && ( {has_sidepeek && sidepeek_content ? (
<DestinationPageSidePeek <DestinationPageSidePeek
buttonText={sidepeek_button_text} buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content} sidePeekContent={sidepeek_content}
location={translatedCountry} location={translatedCountry}
/> />
)} ) : null}
</SidebarContentWrapper> </SidebarContentWrapper>
</aside> </aside>
</div> </div>
)} )}
</CityDataContainer> </DestinationDataProvider>
</Suspense> </Suspense>
<DestinationTracking pageData={tracking} /> <DestinationTracking pageData={tracking} />
</> </>

View File

@@ -14,6 +14,11 @@ interface ExperienceListProps {
export default function ExperienceList({ experiences }: ExperienceListProps) { export default function ExperienceList({ experiences }: ExperienceListProps) {
const intl = useIntl() const intl = useIntl()
if (!experiences.length) {
return null
}
const experienceList = mapExperiencesToListData(experiences, intl) const experienceList = mapExperiencesToListData(experiences, intl)
return ( return (

View File

@@ -1,51 +0,0 @@
import { getHotelsByCityIdentifier } from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
interface HotelDataContainerProps extends React.PropsWithChildren {
cityIdentifier: string
}
export function preload(cityIdentifier: string) {
void getHotelsByCityIdentifier(cityIdentifier)
}
export default async function HotelDataContainer({
cityIdentifier,
children,
}: HotelDataContainerProps) {
const intl = await getIntl()
const hotels = await getHotelsByCityIdentifier(cityIdentifier)
const sortItems: SortItem[] = [
{
label: intl.formatMessage({
defaultMessage: "Distance to city center",
}),
value: SortOption.Distance,
isDefault: true,
},
{
label: intl.formatMessage({
defaultMessage: "Name",
}),
value: SortOption.Name,
},
{
label: intl.formatMessage({
defaultMessage: "TripAdvisor rating",
}),
value: SortOption.TripAdvisorRating,
},
]
return (
<DestinationDataProvider allHotels={hotels} sortItems={sortItems}>
{children}
</DestinationDataProvider>
)
}

View File

@@ -1,52 +1,43 @@
"use client" "use client"
import { useRef } from "react" import { useRef } from "react"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationDataStore } from "@/stores/destination-data"
import { StickyElementNameEnum } from "@/stores/sticky-position" import { StickyElementNameEnum } from "@/stores/sticky-position"
import useStickyPosition from "@/hooks/useStickyPosition" import useStickyPosition from "@/hooks/useStickyPosition"
import { getHeadingText } from "../utils"
import styles from "./sidebarContentWrapper.module.css" import styles from "./sidebarContentWrapper.module.css"
interface SidebarContentWrapperProps extends React.PropsWithChildren { interface SidebarContentWrapperProps extends React.PropsWithChildren {
location: string
preamble: string preamble: string
hasActiveFilter: boolean
heading: string
} }
export default function SidebarContentWrapper({ export default function SidebarContentWrapper({
location,
preamble, preamble,
hasActiveFilter,
heading,
children, children,
}: SidebarContentWrapperProps) { }: SidebarContentWrapperProps) {
const intl = useIntl()
const sidebarRef = useRef<HTMLDivElement>(null) const sidebarRef = useRef<HTMLDivElement>(null)
useStickyPosition({ useStickyPosition({
ref: sidebarRef, ref: sidebarRef,
name: StickyElementNameEnum.DESTINATION_SIDEBAR, name: StickyElementNameEnum.DESTINATION_SIDEBAR,
}) })
const { activeFilters, allFilters } = useDestinationDataStore((state) => ({
activeFilters: state.activeFilters,
allFilters: state.allFilters,
}))
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}> <h1 className={styles.heading}>{heading}</h1>
{getHeadingText(intl, location, allFilters, activeFilters[0])}
</h1>
</Typography> </Typography>
{!activeFilters.length && ( {!hasActiveFilter ? (
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<p className={styles.text}>{preamble}</p> <p className={styles.text}>{preamble}</p>
</Typography> </Typography>
)} ) : null}
{children} {children}
</div> </div>
) )

View File

@@ -14,6 +14,8 @@ import type { DestinationDataProviderProps } from "@/types/providers/destination
export default function DestinationDataProvider({ export default function DestinationDataProvider({
allCities = [], allCities = [],
allHotels, allHotels,
allFilters,
filterFromUrl,
sortItems, sortItems,
children, children,
}: DestinationDataProviderProps) { }: DestinationDataProviderProps) {
@@ -25,6 +27,8 @@ export default function DestinationDataProvider({
storeRef.current = createDestinationDataStore({ storeRef.current = createDestinationDataStore({
allCities, allCities,
allHotels, allHotels,
allFilters,
filterFromUrl,
pathname, pathname,
sortItems, sortItems,
searchParams, searchParams,

View File

@@ -12,7 +12,6 @@ import {
getBasePathNameWithoutFilters, getBasePathNameWithoutFilters,
getFilteredCities, getFilteredCities,
getFilteredHotels, getFilteredHotels,
getFiltersFromHotels,
getSortedCities, getSortedCities,
getSortedHotels, getSortedHotels,
isValidSortOption, isValidSortOption,
@@ -27,18 +26,19 @@ import type {
export function createDestinationDataStore({ export function createDestinationDataStore({
allCities, allCities,
allHotels, allHotels,
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 allFilters = getFiltersFromHotels(allHotels)
const allFilterSlugs = Object.values(allFilters).flatMap((filter: Filter[]) => const allFilterSlugs = Object.values(allFilters).flatMap((filter: Filter[]) =>
filter.map((f) => f.slug) filter.map((f) => f.slug)
) )
const activeFilters: string[] = [] const activeFilters: string[] = filterFromUrl ? [filterFromUrl] : []
const basePathnameWithoutFilters = getBasePathNameWithoutFilters( const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
pathname, pathname,
allFilterSlugs allFilterSlugs

View File

@@ -1,9 +1,14 @@
import type { DestinationPagesHotelData } from "@/types/hotel" import type { DestinationPagesHotelData } from "@/types/hotel"
import type { SortItem } from "../components/destinationFilterAndSort" import type {
CategorizedFilters,
SortItem,
} from "../components/destinationFilterAndSort"
import type { DestinationCityListItem } from "../trpc/routers/contentstack/destinationCityPage" import type { DestinationCityListItem } from "../trpc/routers/contentstack/destinationCityPage"
export interface DestinationDataProviderProps extends React.PropsWithChildren { export interface DestinationDataProviderProps extends React.PropsWithChildren {
allHotels: DestinationPagesHotelData[] allHotels: DestinationPagesHotelData[]
allCities?: DestinationCityListItem[] allCities?: DestinationCityListItem[]
allFilters: CategorizedFilters
filterFromUrl?: string
sortItems: SortItem[] sortItems: SortItem[]
} }

View File

@@ -44,7 +44,11 @@ export interface DestinationDataState {
} }
export interface InitialState export interface InitialState
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> { extends Pick<
DestinationDataState,
"allHotels" | "allCities" | "sortItems" | "allFilters"
> {
pathname: string pathname: string
filterFromUrl?: string
searchParams: ReadonlyURLSearchParams searchParams: ReadonlyURLSearchParams
} }