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 default async function DestinationCityPagePage(
props: PageArgs<{}, { view?: "map" }>
props: PageArgs<{}, { view?: "map"; filterFromUrl?: string }>
) {
const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
@@ -20,7 +20,10 @@ export default async function DestinationCityPagePage(
return (
<Suspense fallback={<DestinationCityPageSkeleton />}>
<DestinationCityPage isMapView={searchParams.view === "map"} />
<DestinationCityPage
isMapView={searchParams.view === "map"}
filterFromUrl={searchParams.filterFromUrl}
/>
</Suspense>
)
}

View File

@@ -10,10 +10,10 @@ import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata"
export default function DestinationCountryPagePage({}: PageArgs<
LangParams,
{ view?: "map" }
>) {
export default async function DestinationCountryPagePage(
props: PageArgs<LangParams, { view?: "map"; filterFromUrl?: string }>
) {
const searchParams = await props.searchParams
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
return notFound()
}
@@ -22,6 +22,7 @@ export default function DestinationCountryPagePage({}: PageArgs<
<Suspense fallback={<DestinationCountryPageSkeleton />}>
<DestinationCountryPage
// isMapView={searchParams.view === "map"} // Disabled until further notice
filterFromUrl={searchParams.filterFromUrl}
isMapView={false}
/>
</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 { 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 BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import Blocks from "../Blocks"
import ExperienceList from "../ExperienceList"
import HotelDataContainer, { preload } from "../HotelDataContainer"
import HotelListing from "../HotelListing"
import SidebarContentWrapper from "../SidebarContentWrapper"
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"
import styles from "./destinationCityPage.module.css"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
interface DestinationCityPageProps {
isMapView: boolean
filterFromUrl?: string
}
export default async function DestinationCityPage({
isMapView,
filterFromUrl,
}: DestinationCityPageProps) {
const intl = await getIntl()
const pageData = await getDestinationCityPage()
if (!pageData) {
@@ -46,12 +58,39 @@ export default async function DestinationCityPage({
destination_settings,
} = 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 (
<>
<Suspense fallback={<DestinationCityPageSkeleton />}>
<HotelDataContainer cityIdentifier={cityIdentifier}>
<DestinationDataProvider
allHotels={allHotels}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
>
{isMapView ? (
<CityMap
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
@@ -74,15 +113,24 @@ export default async function DestinationCityPage({
{blocks && <Blocks blocks={blocks} />}
</main>
<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} />
{has_sidepeek && sidepeek_content && (
{has_sidepeek && sidepeek_content ? (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
location={city.name}
/>
)}
) : null}
{destination_settings.city && (
<StaticMap
@@ -94,7 +142,7 @@ export default async function DestinationCityPage({
</aside>
</div>
)}
</HotelDataContainer>
</DestinationDataProvider>
</Suspense>
<DestinationTracking pageData={tracking} />
</>

View File

@@ -2,31 +2,44 @@ import { notFound } from "next/navigation"
import { Suspense } from "react"
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 BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n"
import DestinationDataProvider from "@/providers/DestinationDataProvider"
import Blocks from "../Blocks"
import CityDataContainer, { preload } from "../CityDataContainer"
import CityListing from "../CityListing"
import ExperienceList from "../ExperienceList"
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"
import styles from "./destinationCountryPage.module.css"
import type { SortItem } from "@/types/components/destinationFilterAndSort"
import { SortOption } from "@/types/enums/destinationFilterAndSort"
interface DestinationCountryPageProps {
isMapView: boolean
filterFromUrl?: string
}
export default async function DestinationCountryPage({
isMapView,
filterFromUrl,
}: DestinationCountryPageProps) {
const intl = await getIntl()
const pageData = await getDestinationCountryPage()
if (!pageData) {
@@ -45,12 +58,38 @@ export default async function DestinationCountryPage({
destination_settings,
} = 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 (
<>
<Suspense fallback={<DestinationCountryPageSkeleton />}>
<CityDataContainer country={destination_settings.country}>
<DestinationDataProvider
allHotels={allHotels}
allCities={allCities}
allFilters={allFilters}
filterFromUrl={filterFromUrl}
sortItems={sortItems}
>
{isMapView ? (
<CountryMap
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
@@ -77,22 +116,28 @@ export default async function DestinationCountryPage({
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper
location={translatedCountry}
hasActiveFilter={!!filterFromUrl}
preamble={preamble}
heading={getHeadingText(
intl,
translatedCountry,
allFilters,
filterFromUrl
)}
>
<ExperienceList experiences={experiences} />
{has_sidepeek && sidepeek_content && (
{has_sidepeek && sidepeek_content ? (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
location={translatedCountry}
/>
)}
) : null}
</SidebarContentWrapper>
</aside>
</div>
)}
</CityDataContainer>
</DestinationDataProvider>
</Suspense>
<DestinationTracking pageData={tracking} />
</>

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import {
getBasePathNameWithoutFilters,
getFilteredCities,
getFilteredHotels,
getFiltersFromHotels,
getSortedCities,
getSortedHotels,
isValidSortOption,
@@ -27,18 +26,19 @@ import type {
export function createDestinationDataStore({
allCities,
allHotels,
allFilters,
filterFromUrl,
pathname,
sortItems,
searchParams,
}: InitialState) {
const defaultSort =
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
const allFilters = getFiltersFromHotels(allHotels)
const allFilterSlugs = Object.values(allFilters).flatMap((filter: Filter[]) =>
filter.map((f) => f.slug)
)
const activeFilters: string[] = []
const activeFilters: string[] = filterFromUrl ? [filterFromUrl] : []
const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
pathname,
allFilterSlugs

View File

@@ -1,9 +1,14 @@
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"
export interface DestinationDataProviderProps extends React.PropsWithChildren {
allHotels: DestinationPagesHotelData[]
allCities?: DestinationCityListItem[]
allFilters: CategorizedFilters
filterFromUrl?: string
sortItems: SortItem[]
}

View File

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