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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user