feat(BOOK-463): Fetching hotel filters from CMS and using these inside the destination pages and select hotel page

* feat(BOOK-463): Fetching hotel filters from CMS and using these inside the destination pages

* fix(BOOK-698): fetch hotel filters from CMS on select hotel page

Approved-by: Bianca Widstam
This commit is contained in:
Erik Tiekstra
2026-01-12 12:02:25 +00:00
parent b2ca2c2612
commit 0c6a4cf186
40 changed files with 732 additions and 399 deletions

View File

@@ -1,150 +0,0 @@
import { logger } from "@scandic-hotels/common/logger"
import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { DestinationFilters } from "../types/destinationsData"
import type {
CategorizedHotelFilters,
HotelFilter,
HotelListingHotelData,
} from "../types/hotel"
const HOTEL_SURROUNDINGS_FILTER_TYPE_NAMES = [
"Hotel surroundings",
"Hotel omgivelser",
"Hotelumgebung",
"Hotellia lähellä",
"Hotellomgivelser",
"Omgivningar",
]
const HOTEL_FACILITIES_FILTER_TYPE_NAMES = [
"Hotel facilities",
"Hotellfaciliteter",
"Hotelfaciliteter",
"Hotel faciliteter",
"Hotel-Infos",
"Hotellin palvelut",
]
function sortFilters(filters: HotelFilter[]): HotelFilter[] {
return [...filters].sort((a, b) => {
// First sort by sortOrder
const orderDiff = a.sortOrder - b.sortOrder
// If sortOrder is the same, sort by name as secondary criterion
return orderDiff === 0 ? a.name.localeCompare(b.name) : orderDiff
})
}
// Merges hotel and SEO filters, removing duplicates (by id).
// In case of duplicates, the SEO filter takes precedence.
function mergeAndDeduplicate(
hotelFilters: 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))
return Array.from(map.values())
}
export function mergeHotelFiltersAndSeoFilters(
hotelFilters: CategorizedHotelFilters,
seoFilters: DestinationFilters
): CategorizedHotelFilters {
if (!seoFilters) {
return hotelFilters
}
return {
...hotelFilters,
facilityFilters: mergeAndDeduplicate(
hotelFilters.facilityFilters,
seoFilters.facilityFilters
),
surroundingsFilters: mergeAndDeduplicate(
hotelFilters.surroundingsFilters,
seoFilters.surroundingsFilters
),
}
}
export function getFiltersFromHotels(
hotels: HotelListingHotelData[],
lang: Lang
): CategorizedHotelFilters {
if (hotels.length === 0) {
return { facilityFilters: [], surroundingsFilters: [], countryFilters: [] }
}
const uniqueCountries = new Set(
hotels.flatMap(({ hotel }) => hotel.countryCode)
)
const countryFilters = [...uniqueCountries]
.map((countryCode) => {
const localizedCountry = getLocalizedCountryByCountryCode(
countryCode,
lang
)
return localizedCountry
? {
name: localizedCountry,
slug: countryCode.toLowerCase(),
filterType: "Country",
sortOrder: 0,
}
: null
})
.filter((filter): filter is HotelFilter => !!filter)
const flattenedFacilityFilters = hotels.flatMap(
({ hotel }) => hotel.detailedFacilities
)
const uniqueFacilityFilterNames = [
...new Set(flattenedFacilityFilters.map((filter) => filter.name)),
]
const facilityFilterList = uniqueFacilityFilterNames
.map((filterName) => {
const filter = flattenedFacilityFilters.find(
(filter) => filter.name === filterName
)
return filter
? {
id: filter.id,
name: filter.name,
slug: filter.slug,
filterType: filter.filter,
sortOrder: filter.sortOrder,
}
: null
})
.filter((filter): filter is HotelFilter => !!filter)
const facilityFilters = facilityFilterList.filter((filter) =>
HOTEL_FACILITIES_FILTER_TYPE_NAMES.includes(filter.filterType)
)
const surroundingsFilters = facilityFilterList.filter((filter) =>
HOTEL_SURROUNDINGS_FILTER_TYPE_NAMES.includes(filter.filterType)
)
return {
facilityFilters: sortFilters(facilityFilters),
surroundingsFilters: sortFilters(surroundingsFilters),
countryFilters: sortFilters(countryFilters),
}
}
function getLocalizedCountryByCountryCode(
countryCode: string,
lang: Lang
): string | null {
const country = new Intl.DisplayNames([lang], { type: "region" })
const localizedCountry = country.of(countryCode)
if (!localizedCountry) {
logger.error(`Could not map ${countryCode} to localized country.`)
return null
}
return localizedCountry
}

View File

@@ -0,0 +1,43 @@
// Merges hotel and SEO filters, removing duplicates (by id).
import type {
HotelFilter,
HotelFilters,
} from "../routers/hotels/filters/output"
import type {
DestinationFilter,
DestinationFilters,
} from "../types/destinationsData"
// In case of duplicates, the SEO filter takes precedence.
function mergeAndDeduplicate(
hotelFilters: HotelFilter[],
seoFilters: DestinationFilter[]
): HotelFilters["facilityFilters"] | HotelFilters["surroundingsFilters"] {
const map = new Map<string, HotelFilter>()
hotelFilters.forEach((filter) => map.set(filter.id, filter))
seoFilters.forEach(({ filter }) => map.set(filter.id, filter))
return Array.from(map.values())
}
export function mergeHotelFiltersAndSeoFilters(
hotelFilters: HotelFilters,
seoFilters: DestinationFilters
): HotelFilters {
if (!seoFilters) {
return hotelFilters
}
return {
...hotelFilters,
facilityFilters: mergeAndDeduplicate(
hotelFilters.facilityFilters,
seoFilters.facilityFilters
),
surroundingsFilters: mergeAndDeduplicate(
hotelFilters.surroundingsFilters,
seoFilters.surroundingsFilters
),
countryFilters: hotelFilters.countryFilters,
}
}