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
@@ -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>
)
}
@@ -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} />
</>
@@ -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} />
</>
@@ -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 (
@@ -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>
)
}
@@ -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>
)