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
}

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

View File

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

View File

@@ -45,6 +45,8 @@ enum AccordionEnum {
DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion",
}
export const accordionSchema = z.object({
@@ -89,6 +91,7 @@ export const accordionSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion:
return (
acc.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
@@ -101,6 +104,7 @@ export const accordionSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion:
return acc.specific_accordion?.questions || []
default:
return null
@@ -170,6 +174,7 @@ export const accordionRefsSchema = z.object({
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsGlobalAccordion:
return (
accordion.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
@@ -184,6 +189,7 @@ export const accordionRefsSchema = z.object({
case AccordionEnum.CampaignPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationFilterBlocksAccordionBlockAccordionsSpecificAccordion:
return (
accordion.specific_accordion?.questions.flatMap((question) =>
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 { 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"
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
.array(
z.object({
heading: z.string().nullish(),
preamble: z.string().nullish(),
blocks: discriminatedUnionArray(blocksSchema.options).nullish(),
filterConnection: z.object({
edges: z.array(
z.object({
@@ -50,27 +78,39 @@ export const destinationFiltersRefsSchema = z
export function transformDestinationFiltersResponse(
data: typeof destinationFiltersSchema._type
) {
const filters = data
?.map(({ filterConnection }) => filterConnection.edges[0]?.node)
const filterData = data
?.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)
if (!data || !filters?.length) {
return null
if (!data || !filterData?.length) {
return {
facilityFilters: [],
surroundingsFilters: [],
}
}
const transformedFilters = filters.map((filter) => ({
id: filter.facility_id,
name: filter.title,
filterType: filter.category,
slug: filter.slug,
sortOrder: 0,
}))
const facilityFilters = transformedFilters.filter(
(f) => f.filterType === "facility"
const facilityFilters = filterData.filter(
(f) => f.filter.filterType === "facility"
)
const surroundingsFilters = transformedFilters.filter(
(f) => f.filterType === "surroundings"
const surroundingsFilters = filterData.filter(
(f) => f.filter.filterType === "surroundings"
)
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 = {
id: string
@@ -17,7 +19,18 @@ export type DestinationCountry = {
export type DestinationsData = DestinationCountry[]
export type SEOFilters = {
facilityFilters: HotelFilter[]
surroundingsFilters: HotelFilter[]
export namespace DestinationFilterBlocksEnum {
export namespace ContentStack {
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 { Lang } from "@scandic-hotels/common/constants/language"
import type { SEOFilters } from "../types/destinationsData"
import type { DestinationFilters } from "../types/destinationsData"
import type {
CategorizedHotelFilters,
HotelFilter,
@@ -39,17 +39,19 @@ function sortFilters(filters: HotelFilter[]): HotelFilter[] {
// In case of duplicates, the SEO filter takes precedence.
function mergeAndDeduplicate(
hotelFilters: HotelFilter[],
seoFilters: HotelFilter[]
seoFilters:
| DestinationFilters["facilityFilters"]
| DestinationFilters["surroundingsFilters"]
): HotelFilter[] {
const map = new Map<FacilityEnum, HotelFilter>()
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())
}
export function mergeHotelFiltersAndSeoFilters(
hotelFilters: CategorizedHotelFilters,
seoFilters: SEOFilters | null
seoFilters: DestinationFilters
): CategorizedHotelFilters {
if (!seoFilters) {
return hotelFilters