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

@@ -4,12 +4,13 @@ import {
type HotelSortItem, type HotelSortItem,
HotelSortOption, HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
import { getHotelsByCSFilter } from "@/lib/trpc/memoizedRequests" import {
getHotelFilters,
getHotelsByCSFilter,
} from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import HotelListingDataProvider from "@/providers/HotelListingDataProvider" import HotelListingDataProvider from "@/providers/HotelListingDataProvider"
import CampaignHotelListingSkeleton from "./CampaignHotelListingSkeleton" import CampaignHotelListingSkeleton from "./CampaignHotelListingSkeleton"
@@ -35,14 +36,13 @@ export default async function CampaignHotelListing({
isMainBlock = false, isMainBlock = false,
}: CampaignHotelListingProps) { }: CampaignHotelListingProps) {
const intl = await getIntl() const intl = await getIntl()
const lang = await getLang()
const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds }) const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds })
if (!hotels.length) { if (!hotels.length) {
return null return null
} }
const allFilters = getFiltersFromHotels(hotels, lang) const allFilters = await getHotelFilters()
const sortItems: HotelSortItem[] = [ const sortItems: HotelSortItem[] = [
{ {
label: intl.formatMessage({ label: intl.formatMessage({

View File

@@ -5,11 +5,11 @@ import {
type HotelSortItem, type HotelSortItem,
HotelSortOption, HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
import { env } from "@/env/server" import { env } from "@/env/server"
import { import {
getDestinationCityPage, getDestinationCityPage,
getHotelFilters,
getHotelsByCityIdentifier, getHotelsByCityIdentifier,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
@@ -17,7 +17,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters" import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import DestinationDataProvider from "@/providers/DestinationDataProvider" import DestinationDataProvider from "@/providers/DestinationDataProvider"
import { getPathname } from "@/utils/getPathname" import { getPathname } from "@/utils/getPathname"
@@ -45,7 +44,6 @@ export default async function DestinationCityPage({
filterFromUrl, filterFromUrl,
}: DestinationCityPageProps) { }: DestinationCityPageProps) {
const intl = await getIntl() const intl = await getIntl()
const lang = await getLang()
const pathname = await getPathname() const pathname = await getPathname()
const pageData = await getDestinationCityPage() const pageData = await getDestinationCityPage()
@@ -70,7 +68,7 @@ export default async function DestinationCityPage({
const activeSeoFilter = getActiveSeoFilter(seo_filters, filterFromUrl) const activeSeoFilter = getActiveSeoFilter(seo_filters, filterFromUrl)
const allHotels = await getHotelsByCityIdentifier(cityIdentifier) const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
const hotelFilters = getFiltersFromHotels(allHotels, lang) const allHotelFilters = await getHotelFilters()
const sortItems: HotelSortItem[] = [ const sortItems: HotelSortItem[] = [
{ {
@@ -102,7 +100,7 @@ export default async function DestinationCityPage({
<Suspense fallback={<DestinationCityPageSkeleton />}> <Suspense fallback={<DestinationCityPageSkeleton />}>
<DestinationDataProvider <DestinationDataProvider
allHotels={allHotels} allHotels={allHotels}
hotelFilters={hotelFilters} hotelFilters={allHotelFilters}
seoFilters={seo_filters} seoFilters={seo_filters}
sortItems={sortItems} sortItems={sortItems}
pathname={pathname} pathname={pathname}

View File

@@ -5,12 +5,12 @@ import {
type HotelSortItem, type HotelSortItem,
HotelSortOption, HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
import { env } from "@/env/server" import { env } from "@/env/server"
import { import {
getDestinationCityPagesByCountry, getDestinationCityPagesByCountry,
getDestinationCountryPage, getDestinationCountryPage,
getHotelFilters,
getHotelsByCountry, getHotelsByCountry,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
@@ -18,7 +18,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters" import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import DestinationDataProvider from "@/providers/DestinationDataProvider" import DestinationDataProvider from "@/providers/DestinationDataProvider"
import { getPathname } from "@/utils/getPathname" import { getPathname } from "@/utils/getPathname"
@@ -46,7 +45,6 @@ export default async function DestinationCountryPage({
filterFromUrl, filterFromUrl,
}: DestinationCountryPageProps) { }: DestinationCountryPageProps) {
const intl = await getIntl() const intl = await getIntl()
const lang = await getLang()
const pathname = await getPathname() const pathname = await getPathname()
const pageData = await getDestinationCountryPage() const pageData = await getDestinationCountryPage()
@@ -74,7 +72,7 @@ export default async function DestinationCountryPage({
getHotelsByCountry(destination_settings.country), getHotelsByCountry(destination_settings.country),
getDestinationCityPagesByCountry(destination_settings.country), getDestinationCityPagesByCountry(destination_settings.country),
]) ])
const hotelFilters = getFiltersFromHotels(allHotels, lang) const allHotelFilters = await getHotelFilters()
const sortItems: HotelSortItem[] = [ const sortItems: HotelSortItem[] = [
{ {
@@ -114,7 +112,7 @@ export default async function DestinationCountryPage({
<DestinationDataProvider <DestinationDataProvider
allHotels={allHotels} allHotels={allHotels}
allCities={allCitiesWithCount} allCities={allCitiesWithCount}
hotelFilters={hotelFilters} hotelFilters={allHotelFilters}
seoFilters={seo_filters} seoFilters={seo_filters}
sortItems={sortItems} sortItems={sortItems}
pathname={pathname} pathname={pathname}

View File

@@ -6,36 +6,38 @@
cursor: pointer; cursor: pointer;
border-radius: var(--Corner-radius-md); border-radius: var(--Corner-radius-md);
transition: background-color 0.3s; transition: background-color 0.3s;
}
.checkboxWrapper:hover { @media (hover: hover) {
background-color: var(--UI-Input-Controls-Surface-Hover); &:not([data-disabled]):hover {
background-color: var(--UI-Input-Controls-Surface-Hover);
}
}
&[data-selected] .checkbox {
border: none;
background-color: var(--Surface-UI-Fill-Active);
}
&[data-disabled] {
cursor: not-allowed;
.checkbox {
border-color: var(--UI-Input-Controls-Border-Disabled);
background-color: var(--UI-Input-Controls-Surface-Disabled);
}
}
} }
.checkbox { .checkbox {
width: 24px; width: 24px;
height: 24px; height: 24px;
min-width: 24px; min-width: 24px;
background: var(--UI-Input-Controls-Surface-Normal);
border: 1px solid var(--UI-Input-Controls-Border-Normal); border: 1px solid var(--UI-Input-Controls-Border-Normal);
border-radius: var(--Corner-radius-sm); border-radius: 4px;
transition: all 0.3s; transition: all 200ms;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: var(--UI-Input-Controls-Surface-Normal); forced-color-adjust: none;
}
.checkboxWrapper[data-selected] .checkbox {
border-color: var(--UI-Input-Controls-Fill-Selected);
background-color: var(--UI-Input-Controls-Fill-Selected);
}
@media screen and (max-width: 767px) {
.checkboxWrapper:hover {
background-color: transparent;
}
.checkboxWrapper[data-selected] {
background-color: transparent;
}
} }

View File

@@ -11,6 +11,7 @@ interface CheckboxProps {
name: string name: string
value: string value: string
isSelected: boolean isSelected: boolean
isDisabled?: boolean
onChange: (filterId: string) => void onChange: (filterId: string) => void
} }
@@ -18,6 +19,7 @@ export default function Checkbox({
isSelected, isSelected,
name, name,
value, value,
isDisabled,
onChange, onChange,
}: CheckboxProps) { }: CheckboxProps) {
return ( return (
@@ -25,6 +27,7 @@ export default function Checkbox({
className={styles.checkboxWrapper} className={styles.checkboxWrapper}
isSelected={isSelected} isSelected={isSelected}
onChange={() => onChange(value)} onChange={() => onChange(value)}
isDisabled={isDisabled}
> >
{({ isSelected }) => ( {({ isSelected }) => (
<> <>

View File

@@ -10,13 +10,17 @@ import Checkbox from "./Checkbox"
import styles from "./filter.module.css" import styles from "./filter.module.css"
import type { CategorizedHotelFilters } from "@scandic-hotels/trpc/types/hotel" import type {
HotelFilter,
HotelFilters,
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
interface FilterProps { interface FilterProps {
filters: CategorizedHotelFilters filters: HotelFilters
listType: "city" | "hotel"
} }
export default function Filter({ filters }: FilterProps) { export default function Filter({ filters, listType }: FilterProps) {
const intl = useIntl() const intl = useIntl()
const { facilityFilters, surroundingsFilters } = filters const { facilityFilters, surroundingsFilters } = filters
const { pendingFilters, togglePendingFilter } = useDestinationDataStore( const { pendingFilters, togglePendingFilter } = useDestinationDataStore(
@@ -54,8 +58,9 @@ export default function Filter({ filters }: FilterProps) {
{facilityFilters.map((filter) => ( {facilityFilters.map((filter) => (
<li key={`filter-${filter.slug}`}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={getCheckboxLabelWithCount(filter, listType)}
value={filter.slug} value={filter.slug}
isDisabled={filter.hotelCount === 0}
onChange={() => togglePendingFilter(filter)} onChange={() => togglePendingFilter(filter)}
isSelected={ isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id) !!pendingFilters.find((pf) => pf.id === filter.id)
@@ -79,8 +84,9 @@ export default function Filter({ filters }: FilterProps) {
{surroundingsFilters.map((filter) => ( {surroundingsFilters.map((filter) => (
<li key={`filter-${filter.slug}`}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={getCheckboxLabelWithCount(filter, listType)}
value={filter.slug} value={filter.slug}
isDisabled={filter.hotelCount === 0}
onChange={() => togglePendingFilter(filter)} onChange={() => togglePendingFilter(filter)}
isSelected={ isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id) !!pendingFilters.find((pf) => pf.id === filter.id)
@@ -94,3 +100,17 @@ export default function Filter({ filters }: FilterProps) {
</div> </div>
) )
} }
function getCheckboxLabelWithCount(
filter: HotelFilter,
listType: "city" | "hotel"
) {
if (listType === "city" && filter.cityCount) {
return `${filter.name} (${filter.cityCount})`
}
if (listType === "hotel" && filter.hotelCount) {
return `${filter.name} (${filter.hotelCount})`
}
return filter.name
}

View File

@@ -34,7 +34,7 @@ export default function DestinationFilterAndSort({
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const { const {
allFilters, filtersWithCount,
sortItems, sortItems,
pendingFilters, pendingFilters,
pendingSort, pendingSort,
@@ -47,7 +47,7 @@ export default function DestinationFilterAndSort({
resetPendingValues, resetPendingValues,
setIsLoading, setIsLoading,
} = useDestinationDataStore((state) => ({ } = useDestinationDataStore((state) => ({
allFilters: state.allFilters, filtersWithCount: state.filtersWithCount,
sortItems: state.sortItems, sortItems: state.sortItems,
pendingFilters: state.pendingFilters, pendingFilters: state.pendingFilters,
pendingSort: state.pendingSort, pendingSort: state.pendingSort,
@@ -166,7 +166,7 @@ export default function DestinationFilterAndSort({
<div className={styles.content}> <div className={styles.content}>
<Sort sortItems={sortItems} /> <Sort sortItems={sortItems} />
<Divider className={styles.divider} /> <Divider className={styles.divider} />
<Filter filters={allFilters} /> <Filter filters={filtersWithCount} listType={listType} />
</div> </div>
{pendingCount === 0 && ( {pendingCount === 0 && (
<div className={styles.alertWrapper}> <div className={styles.alertWrapper}>

View File

@@ -6,36 +6,38 @@
cursor: pointer; cursor: pointer;
border-radius: var(--Corner-radius-md); border-radius: var(--Corner-radius-md);
transition: background-color 0.3s; transition: background-color 0.3s;
}
.checkboxWrapper:hover { @media (hover: hover) {
background-color: var(--UI-Input-Controls-Surface-Hover); &:not([data-disabled]):hover {
background-color: var(--UI-Input-Controls-Surface-Hover);
}
}
&[data-selected] .checkbox {
border: none;
background-color: var(--Surface-UI-Fill-Active);
}
&[data-disabled] {
cursor: not-allowed;
.checkbox {
border-color: var(--UI-Input-Controls-Border-Disabled);
background-color: var(--UI-Input-Controls-Surface-Disabled);
}
}
} }
.checkbox { .checkbox {
width: 24px; width: 24px;
height: 24px; height: 24px;
min-width: 24px; min-width: 24px;
background: var(--UI-Input-Controls-Surface-Normal);
border: 1px solid var(--UI-Input-Controls-Border-Normal); border: 1px solid var(--UI-Input-Controls-Border-Normal);
border-radius: var(--Corner-radius-sm); border-radius: 4px;
transition: all 0.3s; transition: all 200ms;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: var(--UI-Input-Controls-Surface-Normal); forced-color-adjust: none;
}
.checkboxWrapper[data-selected] .checkbox {
border-color: var(--UI-Input-Controls-Fill-Selected);
background-color: var(--UI-Input-Controls-Fill-Selected);
}
@media screen and (max-width: 767px) {
.checkboxWrapper:hover {
background-color: transparent;
}
.checkboxWrapper[data-selected] {
background-color: transparent;
}
} }

View File

@@ -11,6 +11,7 @@ interface CheckboxProps {
name: string name: string
value: string value: string
isSelected: boolean isSelected: boolean
isDisabled?: boolean
onChange: (filterId: string) => void onChange: (filterId: string) => void
} }
@@ -18,12 +19,14 @@ export default function Checkbox({
isSelected, isSelected,
name, name,
value, value,
isDisabled,
onChange, onChange,
}: CheckboxProps) { }: CheckboxProps) {
return ( return (
<AriaCheckbox <AriaCheckbox
className={styles.checkboxWrapper} className={styles.checkboxWrapper}
isSelected={isSelected} isSelected={isSelected}
isDisabled={isDisabled}
onChange={() => onChange(value)} onChange={() => onChange(value)}
> >
{({ isSelected }) => ( {({ isSelected }) => (

View File

@@ -10,10 +10,10 @@ import Checkbox from "./Checkbox"
import styles from "./filter.module.css" import styles from "./filter.module.css"
import type { CategorizedHotelFilters } from "@scandic-hotels/trpc/types/hotel" import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
interface FilterProps { interface FilterProps {
filters: CategorizedHotelFilters filters: HotelFilters
} }
export default function Filter({ filters }: FilterProps) { export default function Filter({ filters }: FilterProps) {
@@ -58,10 +58,17 @@ export default function Filter({ filters }: FilterProps) {
{countryFilters.map((filter) => ( {countryFilters.map((filter) => (
<li key={`filter-${filter.slug}`}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={
filter.hotelCount
? `${filter.name} (${filter.hotelCount})`
: filter.name
}
isDisabled={!filter.hotelCount}
value={filter.slug} value={filter.slug}
onChange={() => togglePendingFilter(filter.slug)} onChange={() => togglePendingFilter(filter)}
isSelected={!!pendingFilters.find((f) => f === filter.slug)} isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id)
}
/> />
</li> </li>
))} ))}
@@ -81,10 +88,17 @@ export default function Filter({ filters }: FilterProps) {
{facilityFilters.map((filter) => ( {facilityFilters.map((filter) => (
<li key={`filter-${filter.slug}`}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={
filter.hotelCount
? `${filter.name} (${filter.hotelCount})`
: filter.name
}
isDisabled={!filter.hotelCount}
value={filter.slug} value={filter.slug}
onChange={() => togglePendingFilter(filter.slug)} onChange={() => togglePendingFilter(filter)}
isSelected={!!pendingFilters.find((f) => f === filter.slug)} isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id)
}
/> />
</li> </li>
))} ))}
@@ -104,10 +118,17 @@ export default function Filter({ filters }: FilterProps) {
{surroundingsFilters.map((filter) => ( {surroundingsFilters.map((filter) => (
<li key={`filter-${filter.slug}`}> <li key={`filter-${filter.slug}`}>
<Checkbox <Checkbox
name={filter.name} name={
filter.hotelCount
? `${filter.name} (${filter.hotelCount})`
: filter.name
}
isDisabled={!filter.hotelCount}
value={filter.slug} value={filter.slug}
onChange={() => togglePendingFilter(filter.slug)} onChange={() => togglePendingFilter(filter)}
isSelected={!!pendingFilters.find((f) => f === filter.slug)} isSelected={
!!pendingFilters.find((pf) => pf.id === filter.id)
}
/> />
</li> </li>
))} ))}

View File

@@ -28,7 +28,7 @@ export default function HotelFilterAndSort() {
const intl = useIntl() const intl = useIntl()
const router = useRouter() const router = useRouter()
const { const {
filters, filtersWithCount,
sortItems, sortItems,
pendingFilters, pendingFilters,
pendingSort, pendingSort,
@@ -39,7 +39,7 @@ export default function HotelFilterAndSort() {
resetPendingValues, resetPendingValues,
setIsLoading, setIsLoading,
} = useHotelListingDataStore((state) => ({ } = useHotelListingDataStore((state) => ({
filters: state.allFilters, filtersWithCount: state.filtersWithCount,
sortItems: state.sortItems, sortItems: state.sortItems,
pendingFilters: state.pendingFilters, pendingFilters: state.pendingFilters,
pendingSort: state.pendingSort, pendingSort: state.pendingSort,
@@ -135,7 +135,7 @@ export default function HotelFilterAndSort() {
<div className={styles.content}> <div className={styles.content}>
<Sort sortItems={sortItems} /> <Sort sortItems={sortItems} />
<Divider className={styles.divider} /> <Divider className={styles.divider} />
<Filter filters={filters} /> <Filter filters={filtersWithCount} />
</div> </div>
{pendingCount === 0 && ( {pendingCount === 0 && (
<div className={styles.alertWrapper}> <div className={styles.alertWrapper}>

View File

@@ -203,6 +203,12 @@ export const getHotelsByCityIdentifier = cache(
}) })
} }
) )
export const getHotelFilters = cache(async function getMemoizedHotelFilters() {
const caller = await serverClient()
return caller.hotel.filters.get()
})
export const getAllHotelData = cache(async function getMemoizedAllHotelData() { export const getAllHotelData = cache(async function getMemoizedAllHotelData() {
const caller = await serverClient() const caller = await serverClient()
return caller.hotel.hotels.getAllHotelData() return caller.hotel.hotels.getAllHotelData()

View File

@@ -8,8 +8,25 @@ import { DestinationDataContext } from "@/contexts/DestinationData"
import DestinationDataProviderContent from "./Content" import DestinationDataProviderContent from "./Content"
import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
import type { DestinationFilters } from "@scandic-hotels/trpc/types/destinationsData"
import type {
HotelListingHotelData,
HotelSortItem,
} from "@scandic-hotels/trpc/types/hotel"
import type { DestinationDataStore } from "@/types/contexts/destination-data" import type { DestinationDataStore } from "@/types/contexts/destination-data"
import type { DestinationDataProviderProps } from "@/types/providers/destination-data"
interface DestinationDataProviderProps extends React.PropsWithChildren {
allHotels: HotelListingHotelData[]
allCities?: DestinationCityListItem[]
hotelFilters: HotelFilters
seoFilters: DestinationFilters
filterFromUrl?: string
sortItems: HotelSortItem[]
pathname: string
}
export default function DestinationDataProvider({ export default function DestinationDataProvider({
allCities = [], allCities = [],

View File

@@ -1,10 +1,13 @@
import { import {
type HotelFilter,
type HotelListingHotelData, type HotelListingHotelData,
type HotelSortItem, type HotelSortItem,
HotelSortOption, HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import type {
HotelFilter,
HotelFilters,
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
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" import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
@@ -32,7 +35,9 @@ export function getFilteredHotels(
if (filters.length) { if (filters.length) {
return hotels.filter(({ hotel }) => return hotels.filter(({ hotel }) =>
filters.every((filter) => filters.every((filter) =>
hotel.detailedFacilities.some((facility) => facility.id === filter.id) hotel.detailedFacilities.some(
(facility) => facility.id === Number(filter.id)
)
) )
) )
} }
@@ -96,3 +101,81 @@ export function getActiveDestinationFilter(
} }
return allSeoFilters.find((f) => f.filter.id === filterFromUrl.id) || null return allSeoFilters.find((f) => f.filter.id === filterFromUrl.id) || null
} }
export function getFiltersWithHotelAndCityCount(
hotelFilters: HotelFilters,
hotels: HotelListingHotelData[],
cities: DestinationCityListItem[]
): HotelFilters {
const flattenedFilters = Object.values(hotelFilters).flat()
const filterHotelCounts: Record<string, number> = {}
const filterCityIdentifiers: Record<string, Set<string>> = {}
hotels.forEach(({ hotel }) => {
hotel.detailedFacilities.forEach((facility) => {
filterHotelCounts[facility.id] = (filterHotelCounts[facility.id] || 0) + 1
if (!filterCityIdentifiers[facility.id]) {
filterCityIdentifiers[facility.id] = new Set()
}
if (hotel.cityIdentifier) {
filterCityIdentifiers[facility.id].add(hotel.cityIdentifier)
}
})
if (hotel.countryCode) {
filterHotelCounts[hotel.countryCode] =
(filterHotelCounts[hotel.countryCode] || 0) + 1
if (!filterCityIdentifiers[hotel.countryCode]) {
filterCityIdentifiers[hotel.countryCode] = new Set()
}
if (hotel.cityIdentifier) {
filterCityIdentifiers[hotel.countryCode].add(hotel.cityIdentifier)
}
}
})
// Count cities that match the cityIdentifiers for each filter
const filterCityCounts: Record<string, number> = {}
Object.entries(filterCityIdentifiers).forEach(
([filterId, cityIdentifiers]) => {
filterCityCounts[filterId] = cities.filter(
(city) =>
city.destination_settings.city &&
cityIdentifiers.has(city.destination_settings.city)
).length
}
)
return flattenedFilters.reduce(
(acc, filter) => {
if (filter.filterType === "facility") {
acc.facilityFilters.push({
...filter,
hotelCount: filterHotelCounts[filter.id] ?? 0,
cityCount: filterCityCounts[filter.id] ?? 0,
})
} else if (filter.filterType === "surroundings") {
acc.surroundingsFilters.push({
...filter,
hotelCount: filterHotelCounts[filter.id] ?? 0,
cityCount: filterCityCounts[filter.id] ?? 0,
})
} else if (filter.filterType === "country") {
acc.countryFilters.push({
...filter,
filterType: "country",
hotelCount: filterHotelCounts[filter.id] ?? 0,
cityCount: filterCityCounts[filter.id] ?? 0,
})
}
return acc
},
{
facilityFilters: [],
surroundingsFilters: [],
countryFilters: [],
} as HotelFilters
)
}

View File

@@ -2,8 +2,8 @@ import { produce } from "immer"
import { useContext } from "react" import { useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
import { getSortedCities } from "@scandic-hotels/trpc/utils/getSortedCities" import { getSortedCities } from "@scandic-hotels/trpc/utils/getSortedCities"
import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/mergeHotelFiltersAndSeoFilters"
import { DestinationDataContext } from "@/contexts/DestinationData" import { DestinationDataContext } from "@/contexts/DestinationData"
import { import {
@@ -16,12 +16,13 @@ import {
getBasePathNameWithoutFilters, getBasePathNameWithoutFilters,
getFilteredCities, getFilteredCities,
getFilteredHotels, getFilteredHotels,
getFiltersWithHotelAndCityCount,
getSortedHotels, getSortedHotels,
isValidSortOption, isValidSortOption,
} from "./helper" } from "./helper"
import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData" import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
import type { import type {
DestinationDataState, DestinationDataState,
@@ -39,9 +40,10 @@ export function createDestinationDataStore({
}: InitialState) { }: InitialState) {
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 allSeoFilters = Object.values(seoFilters).flat()
const allFlattenedFilters = Object.values(allFilters).flat<HotelFilter[]>() const allFlattenedFilters = Object.values(allFilters).flat()
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 activeSeoFilter: DestinationFilter | null = null
@@ -75,6 +77,12 @@ export function createDestinationDataStore({
const filteredCities = getFilteredCities(filteredHotels, allCities) const filteredCities = getFilteredCities(filteredHotels, allCities)
const activeCities = getSortedCities(filteredCities, activeSort) const activeCities = getSortedCities(filteredCities, activeSort)
const filtersWithCount = getFiltersWithHotelAndCityCount(
allFilters,
filteredHotels,
filteredCities
)
return create<DestinationDataState>((set) => ({ return create<DestinationDataState>((set) => ({
actions: { actions: {
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) { updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
@@ -175,9 +183,16 @@ export function createDestinationDataStore({
? getFilteredCities(pendingHotels, state.allCities) ? getFilteredCities(pendingHotels, state.allCities)
: [] : []
const pendingFiltersWithCount = getFiltersWithHotelAndCityCount(
state.allFilters,
pendingHotels,
pendingCities
)
state.pendingFilters = filters state.pendingFilters = filters
state.pendingHotelCount = pendingHotels.length state.pendingHotelCount = pendingHotels.length
state.pendingCityCount = pendingCities.length state.pendingCityCount = pendingCities.length
state.filtersWithCount = pendingFiltersWithCount
}) })
) )
}, },
@@ -187,6 +202,11 @@ export function createDestinationDataStore({
state.pendingFilters = [] state.pendingFilters = []
state.pendingHotelCount = state.allHotels.length state.pendingHotelCount = state.allHotels.length
state.pendingCityCount = state.allCities.length state.pendingCityCount = state.allCities.length
state.filtersWithCount = getFiltersWithHotelAndCityCount(
state.allFilters,
state.allHotels,
state.allCities
)
}) })
) )
}, },
@@ -197,6 +217,19 @@ export function createDestinationDataStore({
state.pendingSort = state.activeSort state.pendingSort = state.activeSort
state.pendingHotelCount = state.activeHotels.length state.pendingHotelCount = state.activeHotels.length
state.pendingCityCount = state.activeCities.length state.pendingCityCount = state.activeCities.length
const filteredHotels = getFilteredHotels(
state.allHotels,
state.activeFilters
)
const filteredCities = getFilteredCities(
filteredHotels,
state.allCities
)
state.filtersWithCount = getFiltersWithHotelAndCityCount(
state.allFilters,
filteredHotels,
filteredCities
)
}) })
) )
}, },
@@ -213,6 +246,7 @@ export function createDestinationDataStore({
activeFilters, activeFilters,
pendingFilters: activeFilters, pendingFilters: activeFilters,
allFilters, allFilters,
filtersWithCount,
activeSeoFilter, activeSeoFilter,
filterFromUrl, filterFromUrl,
basePathnameWithoutFilters, basePathnameWithoutFilters,

View File

@@ -4,6 +4,11 @@ import {
HotelSortOption, HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
import type {
HotelFilter,
HotelFilters,
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
const HOTEL_SORTING_STRATEGIES: Partial< const HOTEL_SORTING_STRATEGIES: Partial<
Record< Record<
HotelSortOption, HotelSortOption,
@@ -20,16 +25,15 @@ const HOTEL_SORTING_STRATEGIES: Partial<
export function getFilteredHotels( export function getFilteredHotels(
hotels: HotelListingHotelData[], hotels: HotelListingHotelData[],
filters: string[] filters: HotelFilter[]
) { ) {
if (filters.length) { if (filters.length) {
return hotels.filter(({ hotel }) => return hotels.filter(({ hotel }) =>
filters.every((filter) => { filters.every((filter) => {
const matchesFacility = hotel.detailedFacilities.some( const matchesFacility = hotel.detailedFacilities.some(
(facility) => facility.slug === filter (facility) => facility.id === Number(filter.id)
) )
const matchesCountry = const matchesCountry = hotel.countryCode === filter.id
hotel.countryCode.toLowerCase() === filter.toLowerCase()
return matchesFacility || matchesCountry return matchesFacility || matchesCountry
}) })
) )
@@ -51,3 +55,50 @@ export function isValidSortOption(
): value is HotelSortOption { ): value is HotelSortOption {
return sortItems.map((item) => item.value).includes(value as HotelSortOption) return sortItems.map((item) => item.value).includes(value as HotelSortOption)
} }
export function getFiltersWithHotelCount(
hotelFilters: HotelFilters,
hotels: HotelListingHotelData[]
): HotelFilters {
const flattenedFilters = Object.values(hotelFilters).flat()
const filterHotelCounts: Record<string, number> = {}
hotels.forEach(({ hotel }) => {
hotel.detailedFacilities.forEach((facility) => {
filterHotelCounts[facility.id] = (filterHotelCounts[facility.id] || 0) + 1
})
if (hotel.countryCode) {
filterHotelCounts[hotel.countryCode] =
(filterHotelCounts[hotel.countryCode] || 0) + 1
}
})
return flattenedFilters.reduce(
(acc, filter) => {
if (filter.filterType === "facility") {
acc.facilityFilters.push({
...filter,
hotelCount: filterHotelCounts[filter.id] ?? 0,
})
} else if (filter.filterType === "surroundings") {
acc.surroundingsFilters.push({
...filter,
hotelCount: filterHotelCounts[filter.id] ?? 0,
})
} else if (filter.filterType === "country") {
acc.countryFilters.push({
...filter,
filterType: "country",
hotelCount: filterHotelCounts[filter.id] ?? 0,
})
}
return acc
},
{
facilityFilters: [],
surroundingsFilters: [],
countryFilters: [],
} as HotelFilters
)
}

View File

@@ -8,9 +8,14 @@ import {
trackSortingChangeEvent, trackSortingChangeEvent,
} from "@/utils/tracking/destinationPage" } from "@/utils/tracking/destinationPage"
import { getFilteredHotels, getSortedHotels, isValidSortOption } from "./helper" import {
getFilteredHotels,
getFiltersWithHotelCount,
getSortedHotels,
isValidSortOption,
} from "./helper"
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel" import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { import type {
HotelListingDataState, HotelListingDataState,
@@ -25,11 +30,10 @@ export function createHotelListingDataStore({
}: InitialState) { }: InitialState) {
const defaultSort = const defaultSort =
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
const allFilterSlugs = Object.values(allFilters).flatMap( const allFlattenedFilters = Object.values(allFilters).flat()
(filter: HotelFilter[]) => filter.map((f) => f.slug) const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
)
const activeFilters: string[] = [] const activeFilters: HotelFilter[] = []
let activeSort = defaultSort let activeSort = defaultSort
if (searchParams) { if (searchParams) {
@@ -43,8 +47,9 @@ export function createHotelListingDataStore({
if (filterParam) { if (filterParam) {
const filters = filterParam.split(",") const filters = filterParam.split(",")
filters.forEach((filter) => { filters.forEach((filter) => {
if (allFilterSlugs.includes(filter)) { const filterFromUrl = allFlattenedFilters.find((f) => f.slug === filter)
activeFilters.push(filter) if (filterFromUrl) {
activeFilters.push(filterFromUrl)
} }
}) })
} }
@@ -52,15 +57,20 @@ export function createHotelListingDataStore({
const filteredHotels = getFilteredHotels(allHotels, activeFilters) const filteredHotels = getFilteredHotels(allHotels, activeFilters)
const activeHotels = getSortedHotels(filteredHotels, activeSort) const activeHotels = getSortedHotels(filteredHotels, activeSort)
const filtersWithCount = getFiltersWithHotelCount(allFilters, filteredHotels)
return create<HotelListingDataState>((set) => ({ return create<HotelListingDataState>((set) => ({
actions: { actions: {
updateActiveFiltersAndSort(filters, sort) { updateActiveFiltersAndSort(filterSlugs, sort) {
return set( return set(
produce((state: HotelListingDataState) => { produce((state: HotelListingDataState) => {
const newSort = const newSort =
sort && isValidSortOption(sort, state.sortItems) sort && isValidSortOption(sort, state.sortItems)
? sort ? sort
: state.defaultSort : state.defaultSort
const filters = allFlattenedFilters.filter((filter) =>
filterSlugs.includes(filter.slug)
)
const filteredHotels = getFilteredHotels(state.allHotels, filters) const filteredHotels = getFilteredHotels(state.allHotels, filters)
const sortedHotels = getSortedHotels(filteredHotels, newSort) const sortedHotels = getSortedHotels(filteredHotels, newSort)
@@ -71,16 +81,22 @@ export function createHotelListingDataStore({
if ( if (
JSON.stringify(filters) !== JSON.stringify(state.activeFilters) JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
) { ) {
const facilityFiltersUsed = filters.filter((f) => const facilityFiltersUsed = filters
state.allFilters.facilityFilters .filter(
.map((ff) => ff.slug) (f) =>
.includes(f) !!state.allFilters.facilityFilters.find(
) (ff) => ff.id === f.id
const surroundingsFiltersUsed = filters.filter((f) => )
state.allFilters.surroundingsFilters )
.map((sf) => sf.slug) .map((f) => f.slug)
.includes(f) const surroundingsFiltersUsed = filters
) .filter(
(f) =>
!!state.allFilters.surroundingsFilters.find(
(sf) => sf.id === f.id
)
)
.map((f) => f.slug)
trackFilterChangeEvent( trackFilterChangeEvent(
facilityFiltersUsed, facilityFiltersUsed,
@@ -116,14 +132,23 @@ export function createHotelListingDataStore({
togglePendingFilter(filter) { togglePendingFilter(filter) {
return set( return set(
produce((state: HotelListingDataState) => { produce((state: HotelListingDataState) => {
const isActive = state.pendingFilters.includes(filter) const filterId = filter.id
const isActive = !!state.pendingFilters.find(
(pf) => pf.id === filterId
)
const filters = isActive const filters = isActive
? state.pendingFilters.filter((f) => f !== filter) ? state.pendingFilters.filter((pf) => pf.id !== filterId)
: [...state.pendingFilters, filter] : [...state.pendingFilters, filter]
const pendingHotels = getFilteredHotels(state.allHotels, filters) const pendingHotels = getFilteredHotels(state.allHotels, filters)
const pendingFiltersWithCount = getFiltersWithHotelCount(
state.allFilters,
pendingHotels
)
state.pendingFilters = filters state.pendingFilters = filters
state.pendingHotelCount = pendingHotels.length state.pendingHotelCount = pendingHotels.length
state.filtersWithCount = pendingFiltersWithCount
}) })
) )
}, },
@@ -132,6 +157,10 @@ export function createHotelListingDataStore({
produce((state: HotelListingDataState) => { produce((state: HotelListingDataState) => {
state.pendingFilters = [] state.pendingFilters = []
state.pendingHotelCount = state.allHotels.length state.pendingHotelCount = state.allHotels.length
state.filtersWithCount = getFiltersWithHotelCount(
state.allFilters,
state.allHotels
)
}) })
) )
}, },
@@ -141,6 +170,10 @@ export function createHotelListingDataStore({
state.pendingFilters = state.activeFilters state.pendingFilters = state.activeFilters
state.pendingSort = state.activeSort state.pendingSort = state.activeSort
state.pendingHotelCount = state.activeHotels.length state.pendingHotelCount = state.activeHotels.length
state.filtersWithCount = getFiltersWithHotelCount(
state.allFilters,
state.activeHotels
)
}) })
) )
}, },
@@ -154,6 +187,7 @@ export function createHotelListingDataStore({
activeFilters, activeFilters,
pendingFilters: activeFilters, pendingFilters: activeFilters,
allFilters, allFilters,
filtersWithCount,
allFilterSlugs, allFilterSlugs,
sortItems, sortItems,
isLoading: false, isLoading: false,

View File

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

View File

@@ -1,11 +1,11 @@
import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { import type {
CategorizedHotelFilters,
HotelListingHotelData, HotelListingHotelData,
HotelSortItem, HotelSortItem,
} from "@scandic-hotels/trpc/types/hotel" } from "@scandic-hotels/trpc/types/hotel"
export interface HotelListingDataProviderProps extends React.PropsWithChildren { export interface HotelListingDataProviderProps extends React.PropsWithChildren {
allHotels: HotelListingHotelData[] allHotels: HotelListingHotelData[]
allFilters: CategorizedHotelFilters allFilters: HotelFilters
sortItems: HotelSortItem[] sortItems: HotelSortItem[]
} }

View File

@@ -1,11 +1,13 @@
import type {
HotelFilter,
HotelFilters,
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage" import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
import type { import type {
DestinationFilter, DestinationFilter,
DestinationFilters, DestinationFilters,
} from "@scandic-hotels/trpc/types/destinationsData" } from "@scandic-hotels/trpc/types/destinationsData"
import type { import type {
CategorizedHotelFilters,
HotelFilter,
HotelListingHotelData, HotelListingHotelData,
HotelSortItem, HotelSortItem,
HotelSortOption, HotelSortOption,
@@ -39,17 +41,20 @@ export interface DestinationDataState {
filterFromUrl: HotelFilter | null filterFromUrl: HotelFilter | null
pendingHotelCount: number pendingHotelCount: number
pendingCityCount: number pendingCityCount: number
allFilters: CategorizedHotelFilters allFilters: HotelFilters
filtersWithCount: HotelFilters
activeSeoFilter: DestinationFilter | null activeSeoFilter: DestinationFilter | null
basePathnameWithoutFilters: string basePathnameWithoutFilters: string
sortItems: HotelSortItem[] sortItems: HotelSortItem[]
isLoading: boolean isLoading: boolean
} }
export interface InitialState export interface InitialState extends Pick<
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> { DestinationDataState,
"allHotels" | "allCities" | "sortItems"
> {
pathname: string pathname: string
searchParams: ReadonlyURLSearchParams searchParams: ReadonlyURLSearchParams
hotelFilters: CategorizedHotelFilters hotelFilters: HotelFilters
seoFilters: DestinationFilters seoFilters: DestinationFilters
} }

View File

@@ -1,5 +1,8 @@
import type { import type {
CategorizedHotelFilters, HotelFilter,
HotelFilters,
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type {
HotelListingHotelData, HotelListingHotelData,
HotelSortItem, HotelSortItem,
HotelSortOption, HotelSortOption,
@@ -9,7 +12,7 @@ import type { ReadonlyURLSearchParams } from "next/navigation"
interface Actions { interface Actions {
updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void
setPendingSort: (sort: HotelSortOption) => void setPendingSort: (sort: HotelSortOption) => void
togglePendingFilter: (filter: string) => void togglePendingFilter: (filter: HotelFilter) => void
clearPendingFilters: () => void clearPendingFilters: () => void
resetPendingValues: () => void resetPendingValues: () => void
setIsLoading: (isLoading: boolean) => void setIsLoading: (isLoading: boolean) => void
@@ -22,19 +25,19 @@ export interface HotelListingDataState {
pendingSort: HotelSortOption pendingSort: HotelSortOption
activeSort: HotelSortOption activeSort: HotelSortOption
defaultSort: HotelSortOption defaultSort: HotelSortOption
pendingFilters: string[] pendingFilters: HotelFilter[]
activeFilters: string[] activeFilters: HotelFilter[]
pendingHotelCount: number pendingHotelCount: number
allFilters: CategorizedHotelFilters allFilters: HotelFilters
filtersWithCount: HotelFilters
allFilterSlugs: string[] allFilterSlugs: string[]
sortItems: HotelSortItem[] sortItems: HotelSortItem[]
isLoading: boolean isLoading: boolean
} }
export interface InitialState export interface InitialState extends Pick<
extends Pick< HotelListingDataState,
HotelListingDataState, "allHotels" | "sortItems" | "allFilters"
"allHotels" | "sortItems" | "allFilters" > {
> {
searchParams: ReadonlyURLSearchParams searchParams: ReadonlyURLSearchParams
} }

View File

@@ -12,7 +12,10 @@ import FilterCheckbox from "./FilterCheckbox"
import styles from "./filterContent.module.css" import styles from "./filterContent.module.css"
import type { CategorizedHotelFilters, HotelFilter } from "../../../../types" import type {
CategorizedHotelFilters,
SelectHotelFilter,
} from "../../../../types"
interface FilterContentProps { interface FilterContentProps {
filters: CategorizedHotelFilters filters: CategorizedHotelFilters
@@ -75,7 +78,7 @@ export default function FilterContent({
return null return null
} }
function filterOutput(filters: HotelFilter[]) { function filterOutput(filters: SelectHotelFilter[]) {
return filters.map((filter) => { return filters.map((filter) => {
const relevantIds = showOnlyBookingCodeRates const relevantIds = showOnlyBookingCodeRates
? filter.bookingCodeFilteredIds ? filter.bookingCodeFilteredIds

View File

@@ -1,5 +1,6 @@
import { dt } from "@scandic-hotels/common/dt" import { dt } from "@scandic-hotels/common/dt"
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel" import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
import { getHotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/utils"
import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers" import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers"
import { serverClient } from "../../trpc" import { serverClient } from "../../trpc"
@@ -18,7 +19,7 @@ import type {
Location, Location,
} from "@scandic-hotels/trpc/types/locations" } from "@scandic-hotels/trpc/types/locations"
import type { CategorizedHotelFilters, HotelFilter } from "../../types" import type { CategorizedHotelFilters, SelectHotelFilter } from "../../types"
type AvailabilityInput = { type AvailabilityInput = {
cityId: string cityId: string
@@ -273,50 +274,38 @@ export async function getHotels({
return hotels return hotels
} }
const hotelSurroundingsFilterNames = [ export async function fetchHotelFiltersAndMapToCategorizedFilters(
"Hotel surroundings",
"Hotel omgivelser",
"Hotelumgebung",
"Hotellia lähellä",
"Hotellomgivelser",
"Omgivningar",
]
const hotelFacilitiesFilterNames = [
"Hotel facilities",
"Hotellfaciliteter",
"Hotelfaciliteter",
"Hotel faciliteter",
"Hotel-Infos",
"Hotellin palvelut",
]
export function getFiltersFromHotels(
hotels: HotelResponse[], hotels: HotelResponse[],
showBookingCodeFilter: boolean showBookingCodeFilter: boolean,
): CategorizedHotelFilters { lang: Lang
): Promise<CategorizedHotelFilters> {
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] } const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
if (!hotels.length) { if (!hotels.length) {
return defaultFilters return defaultFilters
} }
const { countryFilters, ...hotelFilters } = await getHotelFilters(lang)
const allFlattenedFilters = Object.values(hotelFilters).flat()
const filters = hotels.flatMap(({ hotel, availability }) => {
const hotelFilterData = allFlattenedFilters.map((filter) => {
const hotelHasFilter = hotel.detailedFacilities.some(
(facility) => facility.id.toString() === filter.id
)
return {
...filter,
hotelId: hotelHasFilter ? hotel.operaId : null,
hotelIds: hotelHasFilter ? [hotel.operaId] : [],
bookingCodeFilteredIds:
(availability.bookingCode || !showBookingCodeFilter) && hotelHasFilter
? [hotel.operaId]
: [],
}
})
const filters = hotels.flatMap(({ hotel, availability }) => return hotelFilterData
hotel.detailedFacilities.map( })
(facility) =>
<HotelFilter>{
...facility,
hotelId: hotel.operaId,
hotelIds: [hotel.operaId],
bookingCodeFilteredIds:
availability.bookingCode || !showBookingCodeFilter
? [hotel.operaId]
: [],
}
)
)
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
const filterList: HotelFilter[] = uniqueFilterIds const filterList: SelectHotelFilter[] = uniqueFilterIds
.map((filterId) => { .map((filterId) => {
const filter = filters.find((f) => f.id === filterId) const filter = filters.find((f) => f.id === filterId)
@@ -324,7 +313,9 @@ export function getFiltersFromHotels(
if (filter) { if (filter) {
const matchingFilters = filters.filter((f) => f.id === filterId) const matchingFilters = filters.filter((f) => f.id === filterId)
filter.hotelIds = matchingFilters.map((f) => f.hotelId) filter.hotelIds = matchingFilters
.map((f) => f.hotelId)
.filter((id) => id !== null)
filter.bookingCodeFilteredIds = [ filter.bookingCodeFilteredIds = [
...new Set( ...new Set(
matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? []) matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? [])
@@ -333,18 +324,17 @@ export function getFiltersFromHotels(
} }
return filter return filter
}) })
.filter((filter): filter is HotelFilter => filter !== undefined) .filter((filter): filter is SelectHotelFilter => filter !== undefined)
.sort((a, b) => b.sortOrder - a.sortOrder) .sort((a, b) => b.sortOrder - a.sortOrder)
return filterList.reduce<CategorizedHotelFilters>((filters, filter) => { const facilityFilters = filterList.filter(
if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) { (filter) => filter.filterType === "facility"
filters.surroundingsFilters.push(filter) )
} const surroundingsFilters = filterList.filter(
(filter) => filter.filterType === "surroundings"
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) { )
filters.facilityFilters.push(filter) return {
} facilityFilters,
surroundingsFilters,
return filters }
}, defaultFilters)
} }

View File

@@ -9,7 +9,10 @@ import BookingCodeFilter from "../BookingCodeFilter"
import HotelCardListing from "../HotelCardListing" import HotelCardListing from "../HotelCardListing"
import { StaticMap } from "../StaticMap" import { StaticMap } from "../StaticMap"
import HotelFilter from "./Filters/HotelFilter" import HotelFilter from "./Filters/HotelFilter"
import { getFiltersFromHotels, type HotelResponse } from "./helpers" import {
fetchHotelFiltersAndMapToCategorizedFilters,
type HotelResponse,
} from "./helpers"
import HotelCount from "./HotelCount" import HotelCount from "./HotelCount"
import HotelSorter from "./HotelSorter" import HotelSorter from "./HotelSorter"
import { MapWithButtonWrapper } from "./MapWithButtonWrapper" import { MapWithButtonWrapper } from "./MapWithButtonWrapper"
@@ -35,7 +38,7 @@ interface SelectHotelProps {
topSlot?: ReactNode topSlot?: ReactNode
} }
export function SelectHotel({ export async function SelectHotel({
bookingCode, bookingCode,
city, city,
hotels, hotels,
@@ -61,7 +64,11 @@ export function SelectHotel({
const showBookingCodeFilter = isBookingCodeRateAvailable && !isSpecialRate const showBookingCodeFilter = isBookingCodeRateAvailable && !isSpecialRate
const filterList = getFiltersFromHotels(hotels, showBookingCodeFilter) const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
hotels,
showBookingCodeFilter,
lang
)
return ( return (
<> <>

View File

@@ -8,7 +8,7 @@ import { env } from "../../env/server"
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig" import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
import { MapContainer } from "../components/MapContainer" import { MapContainer } from "../components/MapContainer"
import { import {
getFiltersFromHotels, fetchHotelFiltersAndMapToCategorizedFilters,
getHotels, getHotels,
} from "../components/SelectHotel/helpers" } from "../components/SelectHotel/helpers"
import { import {
@@ -108,7 +108,11 @@ export async function AlternativeHotelsMapPage({
config, config,
}) })
const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable) const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
hotels,
isBookingCodeRateAvailable,
lang
)
return ( return (
<BookingFlowConfig config={config}> <BookingFlowConfig config={config}>

View File

@@ -9,7 +9,7 @@ import { env } from "../../env/server"
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig" import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
import { MapContainer } from "../components/MapContainer" import { MapContainer } from "../components/MapContainer"
import { import {
getFiltersFromHotels, fetchHotelFiltersAndMapToCategorizedFilters,
getHotels, getHotels,
} from "../components/SelectHotel/helpers" } from "../components/SelectHotel/helpers"
import { import {
@@ -109,7 +109,11 @@ export async function SelectHotelMapPage({
config, config,
}) })
const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable) const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
hotels,
isBookingCodeRateAvailable,
lang
)
const suspenseKey = stringify(searchParams) const suspenseKey = stringify(searchParams)

View File

@@ -1,14 +1,14 @@
import type { Hotel } from "@scandic-hotels/trpc/types/hotel" import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
export type NextSearchParams = { [key: string]: string | string[] | undefined } export type NextSearchParams = { [key: string]: string | string[] | undefined }
export type HotelFilter = Hotel["detailedFacilities"][number] & { export type SelectHotelFilter = HotelFilter & {
hotelId: string hotelId: string
hotelIds: string[] hotelIds: string[]
bookingCodeFilteredIds: string[] bookingCodeFilteredIds: string[]
} }
export type CategorizedHotelFilters = { export type CategorizedHotelFilters = {
facilityFilters: HotelFilter[] facilityFilters: SelectHotelFilter[]
surroundingsFilters: HotelFilter[] surroundingsFilters: SelectHotelFilter[]
} }

View File

@@ -6,3 +6,12 @@ export enum Country {
Poland = "Poland", Poland = "Poland",
Sweden = "Sweden", Sweden = "Sweden",
} }
export const CountryCode: Record<Country, string> = {
[Country.Denmark]: "DK",
[Country.Finland]: "FI",
[Country.Germany]: "DE",
[Country.Norway]: "NO",
[Country.Poland]: "PL",
[Country.Sweden]: "SE",
}

View File

@@ -8,8 +8,8 @@ export const HotelFilter = gql`
facility_id facility_id
category category
slug slug
sort_order
} }
${System}
` `
export const HotelFilterRef = gql` export const HotelFilterRef = gql`

View File

@@ -0,0 +1,14 @@
import { gql } from "graphql-tag"
import { HotelFilter } from "../Fragments/HotelFilter.graphql"
export const GetHotelFilters = gql`
query GetHotelFilters($locale: String!) {
all_hotel_filter(locale: $locale) {
items {
...HotelFilter
}
}
}
${HotelFilter}
`

View File

@@ -1,16 +1,13 @@
import { ApiCountry } from "../../../types/country" import { ApiCountry } from "../../../types/country"
import { HotelSortOption } from "../../../types/hotel" import { HotelSortOption } from "../../../types/hotel"
import {
getFiltersFromHotels,
mergeHotelFiltersAndSeoFilters,
} from "../../../utils/getFiltersFromHotels"
import { getSortedCities } from "../../../utils/getSortedCities" import { getSortedCities } from "../../../utils/getSortedCities"
import { mergeHotelFiltersAndSeoFilters } from "../../../utils/mergeHotelFiltersAndSeoFilters"
import { getHotelFilters } from "../../hotels/filters/utils"
import { import {
getCityByCityIdentifier, getCityByCityIdentifier,
getHotelIdsByCityIdentifier, getHotelIdsByCityIdentifier,
} from "../../hotels/services/getCityByCityIdentifier" } from "../../hotels/services/getCityByCityIdentifier"
import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry" import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry"
import { getHotelsByHotelIds } from "../../hotels/services/getHotelsByHotelIds"
import { getCityPages } from "../destinationCountryPage/utils" import { getCityPages } from "../destinationCountryPage/utils"
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters" import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
@@ -62,11 +59,9 @@ export async function getCityData(
serviceToken serviceToken
) )
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
let filterType let filterType
if (filter) { if (filter) {
const hotelFilters = getFiltersFromHotels(hotels, lang) const hotelFilters = await getHotelFilters(lang)
const allFilters = mergeHotelFiltersAndSeoFilters( const allFilters = mergeHotelFiltersAndSeoFilters(
hotelFilters, hotelFilters,
seoFilters seoFilters
@@ -104,6 +99,7 @@ export async function getCountryData(
lang: Lang lang: Lang
) { ) {
const country = data.destination_settings?.country const country = data.destination_settings?.country
const seoFilters = transformDestinationFiltersResponse(data.seo_filters)
const filter = input.filterFromUrl const filter = input.filterFromUrl
if (country) { if (country) {
@@ -117,10 +113,13 @@ export async function getCountryData(
serviceToken, serviceToken,
}) })
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
if (filter) { if (filter) {
const allFilters = getFiltersFromHotels(hotels, lang) const hotelFilters = await getHotelFilters(lang)
const allFilters = mergeHotelFiltersAndSeoFilters(
hotelFilters,
seoFilters
)
const facilityFilter = allFilters.facilityFilters.find( const facilityFilter = allFilters.facilityFilters.find(
(f) => f.slug === filter (f) => f.slug === filter
) )

View File

@@ -1,10 +1,10 @@
import { z } from "zod" import { z } from "zod"
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 { DestinationFilterBlocksEnum } from "../../../types/destinationsData"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion" import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import { hotelFilterSchema } from "../../hotels/filters/output"
import { accordionSchema } from "./blocks/accordion" import { accordionSchema } from "./blocks/accordion"
import { contentSchema } from "./blocks/content" import { contentSchema } from "./blocks/content"
import { systemSchema } from "./system" import { systemSchema } from "./system"
@@ -37,12 +37,7 @@ export const destinationFilterSchema = z.object({
filterConnection: z.object({ filterConnection: z.object({
edges: z.array( edges: z.array(
z.object({ z.object({
node: z.object({ node: hotelFilterSchema,
title: z.string(),
facility_id: z.nativeEnum(FacilityEnum).catch(FacilityEnum.UNKNOWN),
category: z.string(),
slug: z.string(),
}),
}) })
), ),
}), }),
@@ -84,13 +79,7 @@ export function transformDestinationFiltersResponse(
heading, heading,
preamble, preamble,
blocks, blocks,
filter: { filter,
id: filter.facility_id,
name: filter.title,
filterType: filter.category,
slug: filter.slug,
sortOrder: 0,
},
} }
}) })
.filter(isDefined) .filter(isDefined)

View File

@@ -0,0 +1,6 @@
import { contentstackBaseProcedure } from "../../../procedures"
import { getHotelFilters } from "./utils"
export const get = contentstackBaseProcedure.query(async ({ ctx }) => {
return getHotelFilters(ctx.lang)
})

View File

@@ -0,0 +1,6 @@
import { router } from "../../.."
import { get } from "./get"
export const filtersRouter = router({
get,
})

View File

@@ -0,0 +1,61 @@
import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
import { getCountryFilters } from "./utils"
export const FILTER_TYPES = ["surroundings", "facility", "country"] as const
export type FilterType = (typeof FILTER_TYPES)[number]
export const hotelFilterSchema = z
.object({
category: z.enum(FILTER_TYPES),
facility_id: z.number().transform((id) => id.toString()),
slug: z.string(),
title: z.string(),
sort_order: z.number().nullish(),
})
.transform(({ facility_id, title, category, slug, sort_order }) => ({
id: facility_id,
name: title,
filterType: category,
slug,
sortOrder: sort_order ?? 0,
hotelCount: 0,
cityCount: 0,
}))
export const hotelFiltersSchema = z
.object({
all_hotel_filter: z.object({
items: z.array(hotelFilterSchema),
}),
lang: z.nativeEnum(Lang),
})
.transform((data) => {
const filters = data.all_hotel_filter.items
if (!filters?.length) {
return {
facilityFilters: [],
surroundingsFilters: [],
countryFilters: [],
}
}
const facilityFilters = filters.filter(
(filter) => filter.filterType === "facility"
)
const surroundingsFilters = filters.filter(
(filter) => filter.filterType === "surroundings"
)
return {
facilityFilters,
surroundingsFilters,
countryFilters: getCountryFilters(data.lang),
}
})
export type HotelFilters = z.output<typeof hotelFiltersSchema>
export type HotelFilter = HotelFilters[keyof HotelFilters][number]

View File

@@ -0,0 +1,98 @@
import { CountryCode } from "@scandic-hotels/common/constants/country"
import { getCacheClient } from "@scandic-hotels/common/dataCache"
import { createCounter } from "@scandic-hotels/common/telemetry"
import { GetHotelFilters } from "../../../graphql/Query/HotelFilters.graphql"
import { request } from "../../../graphql/request"
import { hotelFiltersSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { FilterType, HotelFilter } from "./output"
export async function getHotelFilters(lang: Lang) {
const cacheClient = await getCacheClient()
const cacheKey = `${lang}:getHotelFilters`
return await cacheClient.cacheOrGet(
cacheKey,
async () => {
const getHotelFiltersCounter = createCounter(
"trpc.contentstack.hotelFilters.getAll"
)
const metricsGetHotelFilters = getHotelFiltersCounter.init({ lang })
metricsGetHotelFilters.start()
const response = await request<unknown>(
GetHotelFilters,
{ locale: lang },
{ key: `${lang}:hotel_filters`, ttl: "1d" }
)
if (!response.data) {
metricsGetHotelFilters.noDataError()
return {
facilityFilters: [],
surroundingsFilters: [],
countryFilters: [],
}
}
const validatedResponse = hotelFiltersSchema.safeParse({
...response.data,
lang,
})
if (!validatedResponse.success) {
metricsGetHotelFilters.validationError(validatedResponse.error)
return {
facilityFilters: [],
surroundingsFilters: [],
countryFilters: [],
}
}
metricsGetHotelFilters.success()
const { facilityFilters, surroundingsFilters, countryFilters } =
validatedResponse.data
return {
facilityFilters: sortFilters(facilityFilters),
surroundingsFilters: sortFilters(surroundingsFilters),
countryFilters: sortFilters(countryFilters),
}
},
"1d"
)
}
export function getCountryFilters(lang: Lang) {
const countryCodes = Object.values(CountryCode)
const localizedCountries = new Intl.DisplayNames([lang], { type: "region" })
return [...countryCodes]
.map((countryCode) => {
const localizedCountryName = localizedCountries.of(countryCode)
return localizedCountryName
? {
id: countryCode,
name: localizedCountryName,
slug: countryCode.toLowerCase(),
filterType: "country" as FilterType,
sortOrder: 0,
hotelCount: 0,
cityCount: 0,
}
: null
})
.filter((filter) => !!filter)
}
// First sort by sortOrder descending. The higher the sortOrder, the earlier it appears.
// If sortOrder is the same, sort by name as secondary criterion
export function sortFilters(filters: HotelFilter[]) {
return [...filters].sort((a, b) => {
const orderDiff = b.sortOrder - a.sortOrder
return orderDiff === 0 ? a.name.localeCompare(b.name) : orderDiff
})
}

View File

@@ -47,6 +47,7 @@ import { getHotelsByHotelIds } from "./services/getHotelsByHotelIds"
import { getLocationsByCountries } from "./services/getLocationsByCountries" import { getLocationsByCountries } from "./services/getLocationsByCountries"
import { getPackages } from "./services/getPackages" import { getPackages } from "./services/getPackages"
import { availability } from "./availability" import { availability } from "./availability"
import { filtersRouter } from "./filters"
import { locationsRouter } from "./locations" import { locationsRouter } from "./locations"
import type { HotelListingHotelData } from "../../types/hotel" import type { HotelListingHotelData } from "../../types/hotel"
@@ -331,6 +332,7 @@ export const hotelQueryRouter = router({
env.CACHE_TIME_HOTELS env.CACHE_TIME_HOTELS
) )
}), }),
filters: filtersRouter,
locations: locationsRouter, locations: locationsRouter,
map: router({ map: router({
city: serviceProcedure city: serviceProcedure

View File

@@ -1,4 +1,3 @@
import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import type { z } from "zod" import type { z } from "zod"
import type { import type {
@@ -87,20 +86,6 @@ export type RoomType = Pick<Room, "roomTypes" | "name">
export type RewardNight = z.output<typeof rewardNightSchema> export type RewardNight = z.output<typeof rewardNightSchema>
export interface HotelFilter {
id: FacilityEnum
name: string
slug: string
filterType: string
sortOrder: number
}
export interface CategorizedHotelFilters {
facilityFilters: HotelFilter[]
surroundingsFilters: HotelFilter[]
countryFilters: HotelFilter[]
}
export enum HotelSortOption { export enum HotelSortOption {
Recommended = "recommended", Recommended = "recommended",
Distance = "distance", Distance = "distance",

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,
}
}