feat(BOOK-56): Added content related to destination filters

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-09-25 08:10:30 +00:00
parent 9032789fd0
commit 7714761c77
21 changed files with 379 additions and 172 deletions

View File

@@ -10,15 +10,17 @@ import AccordionSection from "@/components/Blocks/Accordion"
import type { BlocksProps } from "@/types/components/blocks"
export default function Blocks({ blocks }: BlocksProps) {
const { activeFilters } = useDestinationDataStore((state) => ({
activeFilters: state.activeFilters,
const { activeSeoFilter } = useDestinationDataStore((state) => ({
activeSeoFilter: state.activeSeoFilter,
}))
if (activeFilters.length) {
const activeBlocks = activeSeoFilter?.blocks ? activeSeoFilter.blocks : blocks
if (!activeBlocks.length) {
return null
}
return blocks.map((block, idx) => {
return activeBlocks.map((block, idx) => {
switch (block.typename) {
case BlocksEnums.block.Accordion:
return (

View File

@@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationDataStore } from "@/stores/destination-data"
import CityMapContainer from "../../Map/CityMapContainer"
import { getCityHeadingText } from "../../utils"
import { getHeadingText } from "../../utils"
import { BackToCities } from "./BackToCitiesLink"
import HotelList from "./HotelList"
@@ -32,11 +32,10 @@ export default function CityMap({
defaultLocation,
}: CityMapProps) {
const intl = useIntl()
const { activeHotels, allFilters, filterFromUrl } = useDestinationDataStore(
const { activeHotels, activeSeoFilter } = useDestinationDataStore(
(state) => ({
activeHotels: state.activeHotels,
allFilters: state.allFilters,
filterFromUrl: state.filterFromUrl,
activeSeoFilter: state.activeSeoFilter,
})
)
const [fromCountryPage, setIsFromCountryPage] = useState(false)
@@ -58,7 +57,7 @@ export default function CityMap({
{fromCountryPage ? <BackToCities /> : null}
<Typography variant="Title/sm">
<h1 className={styles.title}>
{getCityHeadingText(intl, city.name, allFilters, filterFromUrl)}
{getHeadingText(intl, city.name, "city", activeSeoFilter)}
</h1>
</Typography>
</span>

View File

@@ -117,12 +117,12 @@ export default async function DestinationCityPage({
</div>
<main className={styles.mainContent}>
<HotelListing />
{blocks && <Blocks blocks={blocks} />}
<Blocks blocks={blocks || []} />
<SeoFilters seoFilters={seo_filters} location={city.name} />
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper
preamble={preamble}
defaultPreamble={preamble}
location={city.name}
pageType="city"
>

View File

@@ -7,7 +7,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationDataStore } from "@/stores/destination-data"
import CountryMapContainer from "../../Map/CountryMapContainer"
import { getCountryHeadingText } from "../../utils"
import { getHeadingText } from "../../utils"
import CityList from "./CityList"
import styles from "./countryMap.module.css"
@@ -28,11 +28,10 @@ export default function CountryMap({
defaultLocation,
}: CountryMapProps) {
const intl = useIntl()
const { activeCities, allFilters, filterFromUrl } = useDestinationDataStore(
const { activeCities, activeSeoFilter } = useDestinationDataStore(
(state) => ({
activeCities: state.activeCities,
allFilters: state.allFilters,
filterFromUrl: state.filterFromUrl,
activeSeoFilter: state.activeSeoFilter,
})
)
@@ -45,7 +44,7 @@ export default function CountryMap({
>
<Typography variant="Title/sm">
<h1 className={styles.title}>
{getCountryHeadingText(intl, country, allFilters, filterFromUrl)}
{getHeadingText(intl, country, "country", activeSeoFilter)}
</h1>
</Typography>
<CityList />

View File

@@ -133,7 +133,7 @@ export default async function DestinationCountryPage({
</div>
<main className={styles.mainContent}>
<CityListing />
{blocks && <Blocks blocks={blocks} />}
<Blocks blocks={blocks || []} />
<SeoFilters
seoFilters={seo_filters}
location={translatedCountry}
@@ -141,7 +141,7 @@ export default async function DestinationCountryPage({
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper
preamble={preamble}
defaultPreamble={preamble}
location={translatedCountry}
pageType="country"
>

View File

@@ -10,13 +10,10 @@ import { useDestinationDataStore } from "@/stores/destination-data"
import styles from "./seoFilters.module.css"
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
import type { DestinationFilters } from "@scandic-hotels/trpc/types/destinationsData"
interface SeoFiltersProps {
seoFilters: {
facilityFilters: HotelFilter[]
surroundingsFilters: HotelFilter[]
} | null
seoFilters: DestinationFilters
location: string
}
@@ -25,11 +22,6 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
const { basePath } = useDestinationDataStore((state) => ({
basePath: state.basePathnameWithoutFilters,
}))
if (!seoFilters) {
return null
}
const { facilityFilters, surroundingsFilters } = seoFilters
if (!facilityFilters.length && !surroundingsFilters.length) {
@@ -48,7 +40,7 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
showAsSubtitle
>
<ul className={styles.filterList}>
{facilityFilters.map((filter) => (
{facilityFilters.map(({ filter }) => (
<li key={filter.id}>
<Link
href={`${basePath}/${filter.slug}`}
@@ -71,7 +63,7 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
showAsSubtitle
>
<ul className={styles.filterList}>
{surroundingsFilters.map((filter) => (
{surroundingsFilters.map(({ filter }) => (
<li key={filter.id}>
<Link
href={`${basePath}/${filter.slug}`}

View File

@@ -9,36 +9,37 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationDataStore } from "@/stores/destination-data"
import { getCityHeadingText, getCountryHeadingText } from "../utils"
import {
getHeadingText,
getPreambleText,
} from "@/components/ContentType/DestinationPage/utils"
import styles from "./sidebarContentWrapper.module.css"
interface SidebarContentWrapperProps extends React.PropsWithChildren {
preamble: string
defaultPreamble: string
location: string
pageType: "country" | "city"
}
export default function SidebarContentWrapper({
preamble,
defaultPreamble,
location,
pageType,
children,
}: SidebarContentWrapperProps) {
const intl = useIntl()
const sidebarRef = useRef<HTMLDivElement>(null)
const { allFilters, filterFromUrl } = useDestinationDataStore((state) => ({
allFilters: state.allFilters,
filterFromUrl: state.filterFromUrl,
const { activeSeoFilter } = useDestinationDataStore((state) => ({
activeSeoFilter: state.activeSeoFilter,
}))
useStickyPosition({
ref: sidebarRef,
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
})
const heading =
pageType === "country"
? getCountryHeadingText(intl, location, allFilters, filterFromUrl)
: getCityHeadingText(intl, location, allFilters, filterFromUrl)
const heading = getHeadingText(intl, location, pageType, activeSeoFilter)
const preamble = getPreambleText(defaultPreamble, activeSeoFilter)
return (
<div ref={sidebarRef} className={styles.sidebarContent}>
@@ -46,11 +47,9 @@ export default function SidebarContentWrapper({
<Typography variant="Title/md">
<h1 className={styles.heading}>{heading}</h1>
</Typography>
{!filterFromUrl ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{preamble}</p>
</Typography>
) : null}
<Typography variant="Body/Paragraph/mdRegular">
<p>{preamble}</p>
</Typography>
</div>
{children}
</div>

View File

@@ -1,81 +1,41 @@
import type {
CategorizedHotelFilters,
HotelFilter,
} from "@scandic-hotels/trpc/types/hotel"
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
import type { IntlShape } from "react-intl"
export function getCityHeadingText(
export function getHeadingText(
intl: IntlShape,
location: string,
allFilters: CategorizedHotelFilters,
filterFromUrl: HotelFilter | null
pageType: "country" | "city",
activeSeoFilter: DestinationFilter | null
) {
if (filterFromUrl) {
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.id === filterFromUrl.id
)
const surroudingsFilter = allFilters.surroundingsFilters.find(
(f) => f.id === filterFromUrl.id
)
const defaultHeading =
pageType === "country"
? intl.formatMessage(
{
defaultMessage: "Destinations in {location}",
},
{ location }
)
: intl.formatMessage(
{
defaultMessage: "Hotels in {location}",
},
{ location }
)
if (facilityFilter) {
return intl.formatMessage(
{
defaultMessage: "Hotels with {filter} in {location}",
},
{ location, filter: facilityFilter.name }
)
} else if (surroudingsFilter) {
return intl.formatMessage(
{
defaultMessage: "Hotels near {filter} in {location}",
},
{ location, filter: surroudingsFilter.name }
)
}
if (activeSeoFilter?.heading) {
return activeSeoFilter.heading
}
return intl.formatMessage(
{
defaultMessage: "Hotels in {location}",
},
{ location }
)
return defaultHeading
}
export function getCountryHeadingText(
intl: IntlShape,
location: string,
allFilters: CategorizedHotelFilters,
filterFromUrl: HotelFilter | null
export function getPreambleText(
defaultPreamble: string,
activeSeoFilter: DestinationFilter | null
) {
if (filterFromUrl) {
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.id === filterFromUrl.id
)
const surroudingsFilter = allFilters.surroundingsFilters.find(
(f) => f.id === filterFromUrl.id
)
if (facilityFilter) {
return intl.formatMessage(
{
defaultMessage: "Destinations with {filter} in {location}",
},
{ location, filter: facilityFilter.name }
)
} else if (surroudingsFilter) {
return intl.formatMessage(
{
defaultMessage: "Destinations near {filter} in {location}",
},
{ location, filter: surroudingsFilter.name }
)
}
if (activeSeoFilter) {
return activeSeoFilter.preamble || null
}
return intl.formatMessage(
{
defaultMessage: "Destinations in {location}",
},
{ location }
)
return defaultPreamble
}

View File

@@ -6,6 +6,7 @@ import {
} from "@scandic-hotels/trpc/types/hotel"
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
const HOTEL_SORTING_STRATEGIES: Partial<
Record<
@@ -85,3 +86,13 @@ export function getBasePathNameWithoutFilters(
return pathname
}
export function getActiveDestinationFilter(
filterFromUrl: HotelFilter | null,
allSeoFilters: DestinationFilter[]
) {
if (!filterFromUrl) {
return null
}
return allSeoFilters.find((f) => f.filter.id === filterFromUrl.id) || null
}

View File

@@ -12,6 +12,7 @@ import {
} from "@/utils/tracking/destinationPage"
import {
getActiveDestinationFilter,
getBasePathNameWithoutFilters,
getFilteredCities,
getFilteredHotels,
@@ -19,6 +20,7 @@ import {
isValidSortOption,
} from "./helper"
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
import type {
@@ -38,9 +40,11 @@ export function createDestinationDataStore({
const defaultSort =
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
const allFilters = mergeHotelFiltersAndSeoFilters(hotelFilters, seoFilters)
const allSeoFilters = Object.values(seoFilters).flat()
const allFlattenedFilters = Object.values(allFilters).flat<HotelFilter[]>()
const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
const activeFilters: HotelFilter[] = []
let activeSeoFilter: DestinationFilter | null = null
let filterFromUrl: HotelFilter | null = null
const basePathnameWithoutFilters = getBasePathNameWithoutFilters(
@@ -55,6 +59,7 @@ export function createDestinationDataStore({
) ?? null
if (filterFromUrl) {
activeFilters.push(filterFromUrl)
activeSeoFilter = getActiveDestinationFilter(filterFromUrl, allSeoFilters)
}
}
@@ -129,6 +134,10 @@ export function createDestinationDataStore({
state.activeCities = sortedCities
state.filterFromUrl = filterFromUrl
state.activeSeoFilter = getActiveDestinationFilter(
filterFromUrl,
allSeoFilters
)
state.pendingFilters = filters
state.pendingSort = newSort
state.pendingHotelCount = filteredHotels.length
@@ -204,6 +213,7 @@ export function createDestinationDataStore({
activeFilters,
pendingFilters: activeFilters,
allFilters,
activeSeoFilter,
filterFromUrl,
basePathnameWithoutFilters,
sortItems,

View File

@@ -1,5 +1,5 @@
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
import type { SEOFilters } from "@scandic-hotels/trpc/types/destinationsData"
import type { DestinationFilters } from "@scandic-hotels/trpc/types/destinationsData"
import type {
CategorizedHotelFilters,
HotelListingHotelData,
@@ -10,7 +10,7 @@ export interface DestinationDataProviderProps extends React.PropsWithChildren {
allHotels: HotelListingHotelData[]
allCities?: DestinationCityListItem[]
hotelFilters: CategorizedHotelFilters
seoFilters: SEOFilters | null
seoFilters: DestinationFilters
filterFromUrl?: string
sortItems: HotelSortItem[]
pathname: string

View File

@@ -1,5 +1,8 @@
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
import type { SEOFilters } from "@scandic-hotels/trpc/types/destinationsData"
import type {
DestinationFilter,
DestinationFilters,
} from "@scandic-hotels/trpc/types/destinationsData"
import type {
CategorizedHotelFilters,
HotelFilter,
@@ -37,6 +40,7 @@ export interface DestinationDataState {
pendingHotelCount: number
pendingCityCount: number
allFilters: CategorizedHotelFilters
activeSeoFilter: DestinationFilter | null
basePathnameWithoutFilters: string
sortItems: HotelSortItem[]
isLoading: boolean
@@ -46,6 +50,6 @@ export interface InitialState
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> {
pathname: string
searchParams: ReadonlyURLSearchParams
seoFilters: SEOFilters | null
hotelFilters: CategorizedHotelFilters
seoFilters: DestinationFilters
}