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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,10 @@ import { useDestinationDataStore } from "@/stores/destination-data"
import styles from "./seoFilters.module.css" 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 { interface SeoFiltersProps {
seoFilters: { seoFilters: DestinationFilters
facilityFilters: HotelFilter[]
surroundingsFilters: HotelFilter[]
} | null
location: string location: string
} }
@@ -25,11 +22,6 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
const { basePath } = useDestinationDataStore((state) => ({ const { basePath } = useDestinationDataStore((state) => ({
basePath: state.basePathnameWithoutFilters, basePath: state.basePathnameWithoutFilters,
})) }))
if (!seoFilters) {
return null
}
const { facilityFilters, surroundingsFilters } = seoFilters const { facilityFilters, surroundingsFilters } = seoFilters
if (!facilityFilters.length && !surroundingsFilters.length) { if (!facilityFilters.length && !surroundingsFilters.length) {
@@ -48,7 +40,7 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
showAsSubtitle showAsSubtitle
> >
<ul className={styles.filterList}> <ul className={styles.filterList}>
{facilityFilters.map((filter) => ( {facilityFilters.map(({ filter }) => (
<li key={filter.id}> <li key={filter.id}>
<Link <Link
href={`${basePath}/${filter.slug}`} href={`${basePath}/${filter.slug}`}
@@ -71,7 +63,7 @@ export function SeoFilters({ seoFilters, location }: SeoFiltersProps) {
showAsSubtitle showAsSubtitle
> >
<ul className={styles.filterList}> <ul className={styles.filterList}>
{surroundingsFilters.map((filter) => ( {surroundingsFilters.map(({ filter }) => (
<li key={filter.id}> <li key={filter.id}>
<Link <Link
href={`${basePath}/${filter.slug}`} 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 { useDestinationDataStore } from "@/stores/destination-data"
import { getCityHeadingText, getCountryHeadingText } from "../utils" import {
getHeadingText,
getPreambleText,
} from "@/components/ContentType/DestinationPage/utils"
import styles from "./sidebarContentWrapper.module.css" import styles from "./sidebarContentWrapper.module.css"
interface SidebarContentWrapperProps extends React.PropsWithChildren { interface SidebarContentWrapperProps extends React.PropsWithChildren {
preamble: string defaultPreamble: string
location: string location: string
pageType: "country" | "city" pageType: "country" | "city"
} }
export default function SidebarContentWrapper({ export default function SidebarContentWrapper({
preamble, defaultPreamble,
location, location,
pageType, pageType,
children, children,
}: SidebarContentWrapperProps) { }: SidebarContentWrapperProps) {
const intl = useIntl() const intl = useIntl()
const sidebarRef = useRef<HTMLDivElement>(null) const sidebarRef = useRef<HTMLDivElement>(null)
const { allFilters, filterFromUrl } = useDestinationDataStore((state) => ({ const { activeSeoFilter } = useDestinationDataStore((state) => ({
allFilters: state.allFilters, activeSeoFilter: state.activeSeoFilter,
filterFromUrl: state.filterFromUrl,
})) }))
useStickyPosition({ useStickyPosition({
ref: sidebarRef, ref: sidebarRef,
name: StickyElementNameEnum.DESTINATION_SIDEBAR, name: StickyElementNameEnum.DESTINATION_SIDEBAR,
}) })
const heading =
pageType === "country" const heading = getHeadingText(intl, location, pageType, activeSeoFilter)
? getCountryHeadingText(intl, location, allFilters, filterFromUrl) const preamble = getPreambleText(defaultPreamble, activeSeoFilter)
: getCityHeadingText(intl, location, allFilters, filterFromUrl)
return ( return (
<div ref={sidebarRef} className={styles.sidebarContent}> <div ref={sidebarRef} className={styles.sidebarContent}>
@@ -46,11 +47,9 @@ export default function SidebarContentWrapper({
<Typography variant="Title/md"> <Typography variant="Title/md">
<h1 className={styles.heading}>{heading}</h1> <h1 className={styles.heading}>{heading}</h1>
</Typography> </Typography>
{!filterFromUrl ? ( <Typography variant="Body/Paragraph/mdRegular">
<Typography variant="Body/Paragraph/mdRegular"> <p>{preamble}</p>
<p>{preamble}</p> </Typography>
</Typography>
) : null}
</div> </div>
{children} {children}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -609,3 +609,108 @@ fragment SpecificAccordion_CampaignPageRefs on CampaignPageBlocksAccordionBlockA
} }
} }
} }
fragment Accordion_DestinationFilterBlocks on DestinationFilterBlocksAccordion {
__typename
accordion {
title
accordions {
__typename
...GlobalAccordion_DestinationFilterBlocks
...SpecificAccordion_DestinationFilterBlocks
}
}
}
fragment GlobalAccordion_DestinationFilterBlocks on DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion {
__typename
global_accordion {
global_accordionConnection {
edges {
node {
...AccordionBlock
}
}
}
}
}
fragment SpecificAccordion_DestinationFilterBlocks on DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion {
__typename
specific_accordion {
questions {
question
answer {
json
embedded_itemsConnection {
edges {
node {
__typename
...SysAsset
...AccountPageLink
...CampaignOverviewPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
}
}
}
}
fragment Accordion_DestinationFilterBlocksRefs on DestinationFilterBlocksAccordion {
accordion {
accordions {
__typename
...GlobalAccordion_DestinationFilterBlocksRefs
...SpecificAccordion_DestinationFilterBlocksRefs
}
}
}
fragment GlobalAccordion_DestinationFilterBlocksRefs on DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion {
global_accordion {
global_accordionConnection {
edges {
node {
...AccordionBlockRefs
}
}
}
}
}
fragment SpecificAccordion_DestinationFilterBlocksRefs on DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion {
specific_accordion {
questions {
answer {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignOverviewPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
}
}
}

View File

@@ -233,3 +233,54 @@ fragment Content_DestinationCountryPageRefs on DestinationCountryPageBlocksConte
} }
} }
} }
fragment Content_DestinationFilterBlocks on DestinationFilterBlocksContent {
content {
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageLink
...CampaignOverviewPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
json
}
}
}
fragment Content_DestinationFilterBlocksRefs on DestinationFilterBlocksContent {
content {
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignOverviewPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
#import "./System.graphql"
#import "./HotelFilter.graphql"
#import "./Blocks/Accordion.graphql"
#import "./Blocks/Content.graphql"
fragment DestinationFilter on DestinationFilter {
heading
preamble
blocks {
__typename
...Accordion_DestinationFilterBlocks
...Content_DestinationFilterBlocks
}
filterConnection {
edges {
node {
...HotelFilter
}
}
}
}
fragment DestinationFilterRef on DestinationFilter {
blocks {
__typename
...Accordion_DestinationFilterBlocksRefs
...Content_DestinationFilterBlocksRefs
}
filterConnection {
edges {
node {
...HotelFilterRef
}
}
}
}

View File

@@ -1,6 +1,6 @@
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
#import "../../Fragments/HotelFilter.graphql" #import "../../Fragments/DestinationFilter.graphql"
#import "../../Fragments/Blocks/Accordion.graphql" #import "../../Fragments/Blocks/Accordion.graphql"
#import "../../Fragments/Blocks/Content.graphql" #import "../../Fragments/Blocks/Content.graphql"
@@ -85,13 +85,7 @@ query GetDestinationCityPage($locale: String!, $uid: String!) {
...Content_DestinationCityPage ...Content_DestinationCityPage
} }
seo_filters { seo_filters {
filterConnection { ...DestinationFilter
edges {
node {
...HotelFilter
}
}
}
} }
system { system {
...System ...System
@@ -147,13 +141,7 @@ query GetDestinationCityPageRefs($locale: String!, $uid: String!) {
...Content_DestinationCityPageRefs ...Content_DestinationCityPageRefs
} }
seo_filters { seo_filters {
filterConnection { ...DestinationFilterRef
edges {
node {
...HotelFilterRef
}
}
}
} }
system { system {
...System ...System

View File

@@ -1,6 +1,6 @@
#import "../../Fragments/System.graphql" #import "../../Fragments/System.graphql"
#import "../../Fragments/HotelFilter.graphql" #import "../../Fragments/DestinationFilter.graphql"
#import "../../Fragments/Blocks/Accordion.graphql" #import "../../Fragments/Blocks/Accordion.graphql"
#import "../../Fragments/Blocks/Content.graphql" #import "../../Fragments/Blocks/Content.graphql"
@@ -80,13 +80,7 @@ query GetDestinationCountryPage($locale: String!, $uid: String!) {
...Content_DestinationCountryPage ...Content_DestinationCountryPage
} }
seo_filters { seo_filters {
filterConnection { ...DestinationFilter
edges {
node {
...HotelFilter
}
}
}
} }
system { system {
...System ...System
@@ -129,13 +123,7 @@ query GetDestinationCountryPageRefs($locale: String!, $uid: String!) {
...Content_DestinationCountryPageRefs ...Content_DestinationCountryPageRefs
} }
seo_filters { seo_filters {
filterConnection { ...DestinationFilterRef
edges {
node {
...HotelFilterRef
}
}
}
} }
system { system {
...System ...System

View File

@@ -45,6 +45,8 @@ enum AccordionEnum {
DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion", DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion", DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion", DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion",
} }
export const accordionSchema = z.object({ export const accordionSchema = z.object({
@@ -89,6 +91,7 @@ export const accordionSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion:
return ( return (
acc.global_accordion?.global_accordionConnection.edges.flatMap( acc.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => { ({ node: accordionConnection }) => {
@@ -101,6 +104,7 @@ export const accordionSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion:
return acc.specific_accordion?.questions || [] return acc.specific_accordion?.questions || []
default: default:
return null return null
@@ -170,6 +174,7 @@ export const accordionRefsSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion: case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion:
return ( return (
accordion.global_accordion?.global_accordionConnection.edges.flatMap( accordion.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => { ({ node: accordionConnection }) => {
@@ -184,6 +189,7 @@ export const accordionRefsSchema = z.object({
case AccordionEnum.CampaignPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.CampaignPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion: case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion:
return ( return (
accordion.specific_accordion?.questions.flatMap((question) => accordion.specific_accordion?.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap( question.answer.embedded_itemsConnection.edges.flatMap(

View File

@@ -3,11 +3,39 @@ import { z } from "zod"
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities" import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import { isDefined } from "@scandic-hotels/common/utils/isDefined" import { isDefined } from "@scandic-hotels/common/utils/isDefined"
import { DestinationFilterBlocksEnum } from "../../../types/destinationsData"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import { accordionSchema } from "./blocks/accordion"
import { contentSchema } from "./blocks/content"
import { systemSchema } from "./system" import { systemSchema } from "./system"
export const destinationFilterBlockContent = z
.object({
__typename: z.literal(
DestinationFilterBlocksEnum.ContentStack.blocks.Content
),
})
.merge(contentSchema)
export const destinationFilterBlockAccordion = z
.object({
__typename: z.literal(
DestinationFilterBlocksEnum.ContentStack.blocks.Accordion
),
})
.merge(accordionSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
destinationFilterBlockAccordion,
destinationFilterBlockContent,
])
export const destinationFiltersSchema = z export const destinationFiltersSchema = z
.array( .array(
z.object({ z.object({
heading: z.string().nullish(),
preamble: z.string().nullish(),
blocks: discriminatedUnionArray(blocksSchema.options).nullish(),
filterConnection: z.object({ filterConnection: z.object({
edges: z.array( edges: z.array(
z.object({ z.object({
@@ -50,27 +78,39 @@ export const destinationFiltersRefsSchema = z
export function transformDestinationFiltersResponse( export function transformDestinationFiltersResponse(
data: typeof destinationFiltersSchema._type data: typeof destinationFiltersSchema._type
) { ) {
const filters = data const filterData = data
?.map(({ filterConnection }) => filterConnection.edges[0]?.node) ?.map(({ filterConnection, heading, preamble, blocks }) => {
const filter = filterConnection.edges[0]?.node
if (!filter) {
return null
}
return {
heading,
preamble,
blocks,
filter: {
id: filter.facility_id,
name: filter.title,
filterType: filter.category,
slug: filter.slug,
sortOrder: 0,
},
}
})
.filter(isDefined) .filter(isDefined)
if (!data || !filters?.length) { if (!data || !filterData?.length) {
return null return {
facilityFilters: [],
surroundingsFilters: [],
}
} }
const transformedFilters = filters.map((filter) => ({ const facilityFilters = filterData.filter(
id: filter.facility_id, (f) => f.filter.filterType === "facility"
name: filter.title,
filterType: filter.category,
slug: filter.slug,
sortOrder: 0,
}))
const facilityFilters = transformedFilters.filter(
(f) => f.filterType === "facility"
) )
const surroundingsFilters = transformedFilters.filter( const surroundingsFilters = filterData.filter(
(f) => f.filterType === "surroundings" (f) => f.filter.filterType === "surroundings"
) )
return { return {

View File

@@ -1,4 +1,6 @@
import type { HotelFilter } from "./hotel" import type { z } from "zod"
import type { transformedDestinationFiltersSchema } from "../routers/contentstack/schemas/destinationFilters"
export type City = { export type City = {
id: string id: string
@@ -17,7 +19,18 @@ export type DestinationCountry = {
export type DestinationsData = DestinationCountry[] export type DestinationsData = DestinationCountry[]
export type SEOFilters = { export namespace DestinationFilterBlocksEnum {
facilityFilters: HotelFilter[] export namespace ContentStack {
surroundingsFilters: HotelFilter[] export const enum blocks {
Accordion = "DestinationFilterBlocksAccordion",
Content = "DestinationFilterBlocksContent",
}
}
} }
export type DestinationFilters = z.output<
typeof transformedDestinationFiltersSchema
>
export type DestinationFilter =
DestinationFilters[keyof DestinationFilters][number]

View File

@@ -1,7 +1,7 @@
import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities" import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import type { Lang } from "@scandic-hotels/common/constants/language" import type { Lang } from "@scandic-hotels/common/constants/language"
import type { SEOFilters } from "../types/destinationsData" import type { DestinationFilters } from "../types/destinationsData"
import type { import type {
CategorizedHotelFilters, CategorizedHotelFilters,
HotelFilter, HotelFilter,
@@ -39,17 +39,19 @@ function sortFilters(filters: HotelFilter[]): HotelFilter[] {
// In case of duplicates, the SEO filter takes precedence. // In case of duplicates, the SEO filter takes precedence.
function mergeAndDeduplicate( function mergeAndDeduplicate(
hotelFilters: HotelFilter[], hotelFilters: HotelFilter[],
seoFilters: HotelFilter[] seoFilters:
| DestinationFilters["facilityFilters"]
| DestinationFilters["surroundingsFilters"]
): HotelFilter[] { ): HotelFilter[] {
const map = new Map<FacilityEnum, HotelFilter>() const map = new Map<FacilityEnum, HotelFilter>()
hotelFilters.forEach((filter) => map.set(filter.id, filter)) hotelFilters.forEach((filter) => map.set(filter.id, filter))
seoFilters.forEach((filter) => map.set(filter.id, filter)) seoFilters.forEach(({ filter }) => map.set(filter.id, filter))
return Array.from(map.values()) return Array.from(map.values())
} }
export function mergeHotelFiltersAndSeoFilters( export function mergeHotelFiltersAndSeoFilters(
hotelFilters: CategorizedHotelFilters, hotelFilters: CategorizedHotelFilters,
seoFilters: SEOFilters | null seoFilters: DestinationFilters
): CategorizedHotelFilters { ): CategorizedHotelFilters {
if (!seoFilters) { if (!seoFilters) {
return hotelFilters return hotelFilters