diff --git a/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/checkbox.module.css b/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/checkbox.module.css
index 1ee40afdd..6d99e8c01 100644
--- a/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/checkbox.module.css
+++ b/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/checkbox.module.css
@@ -6,36 +6,38 @@
cursor: pointer;
border-radius: var(--Corner-radius-md);
transition: background-color 0.3s;
-}
-.checkboxWrapper:hover {
- background-color: var(--UI-Input-Controls-Surface-Hover);
+ @media (hover: 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 {
width: 24px;
height: 24px;
min-width: 24px;
+ background: var(--UI-Input-Controls-Surface-Normal);
border: 1px solid var(--UI-Input-Controls-Border-Normal);
- border-radius: var(--Corner-radius-sm);
- transition: all 0.3s;
+ border-radius: 4px;
+ transition: all 200ms;
display: flex;
align-items: center;
justify-content: center;
- background-color: var(--UI-Input-Controls-Surface-Normal);
-}
-
-.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;
- }
+ forced-color-adjust: none;
}
diff --git a/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/index.tsx b/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/index.tsx
index 74ad14a40..3bfe2e7b8 100644
--- a/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/index.tsx
+++ b/apps/scandic-web/components/HotelFilterAndSort/Filter/Checkbox/index.tsx
@@ -11,6 +11,7 @@ interface CheckboxProps {
name: string
value: string
isSelected: boolean
+ isDisabled?: boolean
onChange: (filterId: string) => void
}
@@ -18,12 +19,14 @@ export default function Checkbox({
isSelected,
name,
value,
+ isDisabled,
onChange,
}: CheckboxProps) {
return (
onChange(value)}
>
{({ isSelected }) => (
diff --git a/apps/scandic-web/components/HotelFilterAndSort/Filter/index.tsx b/apps/scandic-web/components/HotelFilterAndSort/Filter/index.tsx
index 476294994..04753c53f 100644
--- a/apps/scandic-web/components/HotelFilterAndSort/Filter/index.tsx
+++ b/apps/scandic-web/components/HotelFilterAndSort/Filter/index.tsx
@@ -10,10 +10,10 @@ import Checkbox from "./Checkbox"
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 {
- filters: CategorizedHotelFilters
+ filters: HotelFilters
}
export default function Filter({ filters }: FilterProps) {
@@ -58,10 +58,17 @@ export default function Filter({ filters }: FilterProps) {
{countryFilters.map((filter) => (
togglePendingFilter(filter.slug)}
- isSelected={!!pendingFilters.find((f) => f === filter.slug)}
+ onChange={() => togglePendingFilter(filter)}
+ isSelected={
+ !!pendingFilters.find((pf) => pf.id === filter.id)
+ }
/>
))}
@@ -81,10 +88,17 @@ export default function Filter({ filters }: FilterProps) {
{facilityFilters.map((filter) => (
togglePendingFilter(filter.slug)}
- isSelected={!!pendingFilters.find((f) => f === filter.slug)}
+ onChange={() => togglePendingFilter(filter)}
+ isSelected={
+ !!pendingFilters.find((pf) => pf.id === filter.id)
+ }
/>
))}
@@ -104,10 +118,17 @@ export default function Filter({ filters }: FilterProps) {
{surroundingsFilters.map((filter) => (
togglePendingFilter(filter.slug)}
- isSelected={!!pendingFilters.find((f) => f === filter.slug)}
+ onChange={() => togglePendingFilter(filter)}
+ isSelected={
+ !!pendingFilters.find((pf) => pf.id === filter.id)
+ }
/>
))}
diff --git a/apps/scandic-web/components/HotelFilterAndSort/index.tsx b/apps/scandic-web/components/HotelFilterAndSort/index.tsx
index b3cd90ed6..fc7cc4dfb 100644
--- a/apps/scandic-web/components/HotelFilterAndSort/index.tsx
+++ b/apps/scandic-web/components/HotelFilterAndSort/index.tsx
@@ -28,7 +28,7 @@ export default function HotelFilterAndSort() {
const intl = useIntl()
const router = useRouter()
const {
- filters,
+ filtersWithCount,
sortItems,
pendingFilters,
pendingSort,
@@ -39,7 +39,7 @@ export default function HotelFilterAndSort() {
resetPendingValues,
setIsLoading,
} = useHotelListingDataStore((state) => ({
- filters: state.allFilters,
+ filtersWithCount: state.filtersWithCount,
sortItems: state.sortItems,
pendingFilters: state.pendingFilters,
pendingSort: state.pendingSort,
@@ -135,7 +135,7 @@ export default function HotelFilterAndSort() {
{pendingCount === 0 && (
diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
index 1a77b3ec2..6f474239d 100644
--- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
+++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts
@@ -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() {
const caller = await serverClient()
return caller.hotel.hotels.getAllHotelData()
diff --git a/apps/scandic-web/providers/DestinationDataProvider/index.tsx b/apps/scandic-web/providers/DestinationDataProvider/index.tsx
index 4ea331129..af0fdfc71 100644
--- a/apps/scandic-web/providers/DestinationDataProvider/index.tsx
+++ b/apps/scandic-web/providers/DestinationDataProvider/index.tsx
@@ -8,8 +8,25 @@ import { DestinationDataContext } from "@/contexts/DestinationData"
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 { 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({
allCities = [],
diff --git a/apps/scandic-web/stores/destination-data/helper.ts b/apps/scandic-web/stores/destination-data/helper.ts
index a332b1621..aa53a23a0 100644
--- a/apps/scandic-web/stores/destination-data/helper.ts
+++ b/apps/scandic-web/stores/destination-data/helper.ts
@@ -1,10 +1,13 @@
import {
- type HotelFilter,
type HotelListingHotelData,
type HotelSortItem,
HotelSortOption,
} 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 { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
@@ -32,7 +35,9 @@ export function getFilteredHotels(
if (filters.length) {
return hotels.filter(({ hotel }) =>
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
}
+
+export function getFiltersWithHotelAndCityCount(
+ hotelFilters: HotelFilters,
+ hotels: HotelListingHotelData[],
+ cities: DestinationCityListItem[]
+): HotelFilters {
+ const flattenedFilters = Object.values(hotelFilters).flat()
+ const filterHotelCounts: Record = {}
+ const filterCityIdentifiers: Record> = {}
+
+ 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 = {}
+ 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
+ )
+}
diff --git a/apps/scandic-web/stores/destination-data/index.ts b/apps/scandic-web/stores/destination-data/index.ts
index 06f1d18ef..9a3006b9c 100644
--- a/apps/scandic-web/stores/destination-data/index.ts
+++ b/apps/scandic-web/stores/destination-data/index.ts
@@ -2,8 +2,8 @@ import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
-import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
import { getSortedCities } from "@scandic-hotels/trpc/utils/getSortedCities"
+import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/mergeHotelFiltersAndSeoFilters"
import { DestinationDataContext } from "@/contexts/DestinationData"
import {
@@ -16,12 +16,13 @@ import {
getBasePathNameWithoutFilters,
getFilteredCities,
getFilteredHotels,
+ getFiltersWithHotelAndCityCount,
getSortedHotels,
isValidSortOption,
} from "./helper"
+import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
-import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
import type {
DestinationDataState,
@@ -39,9 +40,10 @@ export function createDestinationDataStore({
}: InitialState) {
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()
+ const allFlattenedFilters = Object.values(allFilters).flat()
const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
const activeFilters: HotelFilter[] = []
let activeSeoFilter: DestinationFilter | null = null
@@ -75,6 +77,12 @@ export function createDestinationDataStore({
const filteredCities = getFilteredCities(filteredHotels, allCities)
const activeCities = getSortedCities(filteredCities, activeSort)
+ const filtersWithCount = getFiltersWithHotelAndCityCount(
+ allFilters,
+ filteredHotels,
+ filteredCities
+ )
+
return create((set) => ({
actions: {
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
@@ -175,9 +183,16 @@ export function createDestinationDataStore({
? getFilteredCities(pendingHotels, state.allCities)
: []
+ const pendingFiltersWithCount = getFiltersWithHotelAndCityCount(
+ state.allFilters,
+ pendingHotels,
+ pendingCities
+ )
+
state.pendingFilters = filters
state.pendingHotelCount = pendingHotels.length
state.pendingCityCount = pendingCities.length
+ state.filtersWithCount = pendingFiltersWithCount
})
)
},
@@ -187,6 +202,11 @@ export function createDestinationDataStore({
state.pendingFilters = []
state.pendingHotelCount = state.allHotels.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.pendingHotelCount = state.activeHotels.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,
pendingFilters: activeFilters,
allFilters,
+ filtersWithCount,
activeSeoFilter,
filterFromUrl,
basePathnameWithoutFilters,
diff --git a/apps/scandic-web/stores/hotel-listing-data/helper.ts b/apps/scandic-web/stores/hotel-listing-data/helper.ts
index 9900dae1e..fd3362d41 100644
--- a/apps/scandic-web/stores/hotel-listing-data/helper.ts
+++ b/apps/scandic-web/stores/hotel-listing-data/helper.ts
@@ -4,6 +4,11 @@ import {
HotelSortOption,
} from "@scandic-hotels/trpc/types/hotel"
+import type {
+ HotelFilter,
+ HotelFilters,
+} from "@scandic-hotels/trpc/routers/hotels/filters/output"
+
const HOTEL_SORTING_STRATEGIES: Partial<
Record<
HotelSortOption,
@@ -20,16 +25,15 @@ const HOTEL_SORTING_STRATEGIES: Partial<
export function getFilteredHotels(
hotels: HotelListingHotelData[],
- filters: string[]
+ filters: HotelFilter[]
) {
if (filters.length) {
return hotels.filter(({ hotel }) =>
filters.every((filter) => {
const matchesFacility = hotel.detailedFacilities.some(
- (facility) => facility.slug === filter
+ (facility) => facility.id === Number(filter.id)
)
- const matchesCountry =
- hotel.countryCode.toLowerCase() === filter.toLowerCase()
+ const matchesCountry = hotel.countryCode === filter.id
return matchesFacility || matchesCountry
})
)
@@ -51,3 +55,50 @@ export function isValidSortOption(
): value is 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 = {}
+
+ 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
+ )
+}
diff --git a/apps/scandic-web/stores/hotel-listing-data/index.ts b/apps/scandic-web/stores/hotel-listing-data/index.ts
index ae4fe91e1..585e7efc3 100644
--- a/apps/scandic-web/stores/hotel-listing-data/index.ts
+++ b/apps/scandic-web/stores/hotel-listing-data/index.ts
@@ -8,9 +8,14 @@ import {
trackSortingChangeEvent,
} 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 {
HotelListingDataState,
@@ -25,11 +30,10 @@ export function createHotelListingDataStore({
}: InitialState) {
const defaultSort =
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
- const allFilterSlugs = Object.values(allFilters).flatMap(
- (filter: HotelFilter[]) => filter.map((f) => f.slug)
- )
+ const allFlattenedFilters = Object.values(allFilters).flat()
+ const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
- const activeFilters: string[] = []
+ const activeFilters: HotelFilter[] = []
let activeSort = defaultSort
if (searchParams) {
@@ -43,8 +47,9 @@ export function createHotelListingDataStore({
if (filterParam) {
const filters = filterParam.split(",")
filters.forEach((filter) => {
- if (allFilterSlugs.includes(filter)) {
- activeFilters.push(filter)
+ const filterFromUrl = allFlattenedFilters.find((f) => f.slug === filter)
+ if (filterFromUrl) {
+ activeFilters.push(filterFromUrl)
}
})
}
@@ -52,15 +57,20 @@ export function createHotelListingDataStore({
const filteredHotels = getFilteredHotels(allHotels, activeFilters)
const activeHotels = getSortedHotels(filteredHotels, activeSort)
+ const filtersWithCount = getFiltersWithHotelCount(allFilters, filteredHotels)
+
return create((set) => ({
actions: {
- updateActiveFiltersAndSort(filters, sort) {
+ updateActiveFiltersAndSort(filterSlugs, sort) {
return set(
produce((state: HotelListingDataState) => {
const newSort =
sort && isValidSortOption(sort, state.sortItems)
? sort
: state.defaultSort
+ const filters = allFlattenedFilters.filter((filter) =>
+ filterSlugs.includes(filter.slug)
+ )
const filteredHotels = getFilteredHotels(state.allHotels, filters)
const sortedHotels = getSortedHotels(filteredHotels, newSort)
@@ -71,16 +81,22 @@ export function createHotelListingDataStore({
if (
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
) {
- const facilityFiltersUsed = filters.filter((f) =>
- state.allFilters.facilityFilters
- .map((ff) => ff.slug)
- .includes(f)
- )
- const surroundingsFiltersUsed = filters.filter((f) =>
- state.allFilters.surroundingsFilters
- .map((sf) => sf.slug)
- .includes(f)
- )
+ const facilityFiltersUsed = filters
+ .filter(
+ (f) =>
+ !!state.allFilters.facilityFilters.find(
+ (ff) => ff.id === f.id
+ )
+ )
+ .map((f) => f.slug)
+ const surroundingsFiltersUsed = filters
+ .filter(
+ (f) =>
+ !!state.allFilters.surroundingsFilters.find(
+ (sf) => sf.id === f.id
+ )
+ )
+ .map((f) => f.slug)
trackFilterChangeEvent(
facilityFiltersUsed,
@@ -116,14 +132,23 @@ export function createHotelListingDataStore({
togglePendingFilter(filter) {
return set(
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
- ? state.pendingFilters.filter((f) => f !== filter)
+ ? state.pendingFilters.filter((pf) => pf.id !== filterId)
: [...state.pendingFilters, filter]
const pendingHotels = getFilteredHotels(state.allHotels, filters)
+ const pendingFiltersWithCount = getFiltersWithHotelCount(
+ state.allFilters,
+ pendingHotels
+ )
+
state.pendingFilters = filters
state.pendingHotelCount = pendingHotels.length
+ state.filtersWithCount = pendingFiltersWithCount
})
)
},
@@ -132,6 +157,10 @@ export function createHotelListingDataStore({
produce((state: HotelListingDataState) => {
state.pendingFilters = []
state.pendingHotelCount = state.allHotels.length
+ state.filtersWithCount = getFiltersWithHotelCount(
+ state.allFilters,
+ state.allHotels
+ )
})
)
},
@@ -141,6 +170,10 @@ export function createHotelListingDataStore({
state.pendingFilters = state.activeFilters
state.pendingSort = state.activeSort
state.pendingHotelCount = state.activeHotels.length
+ state.filtersWithCount = getFiltersWithHotelCount(
+ state.allFilters,
+ state.activeHotels
+ )
})
)
},
@@ -154,6 +187,7 @@ export function createHotelListingDataStore({
activeFilters,
pendingFilters: activeFilters,
allFilters,
+ filtersWithCount,
allFilterSlugs,
sortItems,
isLoading: false,
diff --git a/apps/scandic-web/types/providers/destination-data.ts b/apps/scandic-web/types/providers/destination-data.ts
deleted file mode 100644
index c2bf7355f..000000000
--- a/apps/scandic-web/types/providers/destination-data.ts
+++ /dev/null
@@ -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
-}
diff --git a/apps/scandic-web/types/providers/hotel-listing-data.ts b/apps/scandic-web/types/providers/hotel-listing-data.ts
index bc52c83b9..e181008a8 100644
--- a/apps/scandic-web/types/providers/hotel-listing-data.ts
+++ b/apps/scandic-web/types/providers/hotel-listing-data.ts
@@ -1,11 +1,11 @@
+import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
import type {
- CategorizedHotelFilters,
HotelListingHotelData,
HotelSortItem,
} from "@scandic-hotels/trpc/types/hotel"
export interface HotelListingDataProviderProps extends React.PropsWithChildren {
allHotels: HotelListingHotelData[]
- allFilters: CategorizedHotelFilters
+ allFilters: HotelFilters
sortItems: HotelSortItem[]
}
diff --git a/apps/scandic-web/types/stores/destination-data.ts b/apps/scandic-web/types/stores/destination-data.ts
index 89c9ee600..5a576b239 100644
--- a/apps/scandic-web/types/stores/destination-data.ts
+++ b/apps/scandic-web/types/stores/destination-data.ts
@@ -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 {
DestinationFilter,
DestinationFilters,
} from "@scandic-hotels/trpc/types/destinationsData"
import type {
- CategorizedHotelFilters,
- HotelFilter,
HotelListingHotelData,
HotelSortItem,
HotelSortOption,
@@ -39,17 +41,20 @@ export interface DestinationDataState {
filterFromUrl: HotelFilter | null
pendingHotelCount: number
pendingCityCount: number
- allFilters: CategorizedHotelFilters
+ allFilters: HotelFilters
+ filtersWithCount: HotelFilters
activeSeoFilter: DestinationFilter | null
basePathnameWithoutFilters: string
sortItems: HotelSortItem[]
isLoading: boolean
}
-export interface InitialState
- extends Pick {
+export interface InitialState extends Pick<
+ DestinationDataState,
+ "allHotels" | "allCities" | "sortItems"
+> {
pathname: string
searchParams: ReadonlyURLSearchParams
- hotelFilters: CategorizedHotelFilters
+ hotelFilters: HotelFilters
seoFilters: DestinationFilters
}
diff --git a/apps/scandic-web/types/stores/hotel-listing-data.ts b/apps/scandic-web/types/stores/hotel-listing-data.ts
index b640774b1..b9aa68d5b 100644
--- a/apps/scandic-web/types/stores/hotel-listing-data.ts
+++ b/apps/scandic-web/types/stores/hotel-listing-data.ts
@@ -1,5 +1,8 @@
import type {
- CategorizedHotelFilters,
+ HotelFilter,
+ HotelFilters,
+} from "@scandic-hotels/trpc/routers/hotels/filters/output"
+import type {
HotelListingHotelData,
HotelSortItem,
HotelSortOption,
@@ -9,7 +12,7 @@ import type { ReadonlyURLSearchParams } from "next/navigation"
interface Actions {
updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void
setPendingSort: (sort: HotelSortOption) => void
- togglePendingFilter: (filter: string) => void
+ togglePendingFilter: (filter: HotelFilter) => void
clearPendingFilters: () => void
resetPendingValues: () => void
setIsLoading: (isLoading: boolean) => void
@@ -22,19 +25,19 @@ export interface HotelListingDataState {
pendingSort: HotelSortOption
activeSort: HotelSortOption
defaultSort: HotelSortOption
- pendingFilters: string[]
- activeFilters: string[]
+ pendingFilters: HotelFilter[]
+ activeFilters: HotelFilter[]
pendingHotelCount: number
- allFilters: CategorizedHotelFilters
+ allFilters: HotelFilters
+ filtersWithCount: HotelFilters
allFilterSlugs: string[]
sortItems: HotelSortItem[]
isLoading: boolean
}
-export interface InitialState
- extends Pick<
- HotelListingDataState,
- "allHotels" | "sortItems" | "allFilters"
- > {
+export interface InitialState extends Pick<
+ HotelListingDataState,
+ "allHotels" | "sortItems" | "allFilters"
+> {
searchParams: ReadonlyURLSearchParams
}
diff --git a/packages/booking-flow/lib/components/SelectHotel/Filters/FilterContent/index.tsx b/packages/booking-flow/lib/components/SelectHotel/Filters/FilterContent/index.tsx
index 9fd75fa09..c3a224a1a 100644
--- a/packages/booking-flow/lib/components/SelectHotel/Filters/FilterContent/index.tsx
+++ b/packages/booking-flow/lib/components/SelectHotel/Filters/FilterContent/index.tsx
@@ -12,7 +12,10 @@ import FilterCheckbox from "./FilterCheckbox"
import styles from "./filterContent.module.css"
-import type { CategorizedHotelFilters, HotelFilter } from "../../../../types"
+import type {
+ CategorizedHotelFilters,
+ SelectHotelFilter,
+} from "../../../../types"
interface FilterContentProps {
filters: CategorizedHotelFilters
@@ -75,7 +78,7 @@ export default function FilterContent({
return null
}
- function filterOutput(filters: HotelFilter[]) {
+ function filterOutput(filters: SelectHotelFilter[]) {
return filters.map((filter) => {
const relevantIds = showOnlyBookingCodeRates
? filter.bookingCodeFilteredIds
diff --git a/packages/booking-flow/lib/components/SelectHotel/helpers.ts b/packages/booking-flow/lib/components/SelectHotel/helpers.ts
index f0bf4b99f..a75d8e651 100644
--- a/packages/booking-flow/lib/components/SelectHotel/helpers.ts
+++ b/packages/booking-flow/lib/components/SelectHotel/helpers.ts
@@ -1,5 +1,6 @@
import { dt } from "@scandic-hotels/common/dt"
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 { serverClient } from "../../trpc"
@@ -18,7 +19,7 @@ import type {
Location,
} from "@scandic-hotels/trpc/types/locations"
-import type { CategorizedHotelFilters, HotelFilter } from "../../types"
+import type { CategorizedHotelFilters, SelectHotelFilter } from "../../types"
type AvailabilityInput = {
cityId: string
@@ -273,50 +274,38 @@ export async function getHotels({
return hotels
}
-const hotelSurroundingsFilterNames = [
- "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(
+export async function fetchHotelFiltersAndMapToCategorizedFilters(
hotels: HotelResponse[],
- showBookingCodeFilter: boolean
-): CategorizedHotelFilters {
+ showBookingCodeFilter: boolean,
+ lang: Lang
+): Promise {
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
if (!hotels.length) {
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 }) =>
- hotel.detailedFacilities.map(
- (facility) =>
- {
- ...facility,
- hotelId: hotel.operaId,
- hotelIds: [hotel.operaId],
- bookingCodeFilteredIds:
- availability.bookingCode || !showBookingCodeFilter
- ? [hotel.operaId]
- : [],
- }
- )
- )
+ return hotelFilterData
+ })
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
- const filterList: HotelFilter[] = uniqueFilterIds
+ const filterList: SelectHotelFilter[] = uniqueFilterIds
.map((filterId) => {
const filter = filters.find((f) => f.id === filterId)
@@ -324,7 +313,9 @@ export function getFiltersFromHotels(
if (filter) {
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 = [
...new Set(
matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? [])
@@ -333,18 +324,17 @@ export function getFiltersFromHotels(
}
return filter
})
- .filter((filter): filter is HotelFilter => filter !== undefined)
+ .filter((filter): filter is SelectHotelFilter => filter !== undefined)
.sort((a, b) => b.sortOrder - a.sortOrder)
- return filterList.reduce((filters, filter) => {
- if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) {
- filters.surroundingsFilters.push(filter)
- }
-
- if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) {
- filters.facilityFilters.push(filter)
- }
-
- return filters
- }, defaultFilters)
+ const facilityFilters = filterList.filter(
+ (filter) => filter.filterType === "facility"
+ )
+ const surroundingsFilters = filterList.filter(
+ (filter) => filter.filterType === "surroundings"
+ )
+ return {
+ facilityFilters,
+ surroundingsFilters,
+ }
}
diff --git a/packages/booking-flow/lib/components/SelectHotel/index.tsx b/packages/booking-flow/lib/components/SelectHotel/index.tsx
index 60a81a6f4..a1ab40a09 100644
--- a/packages/booking-flow/lib/components/SelectHotel/index.tsx
+++ b/packages/booking-flow/lib/components/SelectHotel/index.tsx
@@ -9,7 +9,10 @@ import BookingCodeFilter from "../BookingCodeFilter"
import HotelCardListing from "../HotelCardListing"
import { StaticMap } from "../StaticMap"
import HotelFilter from "./Filters/HotelFilter"
-import { getFiltersFromHotels, type HotelResponse } from "./helpers"
+import {
+ fetchHotelFiltersAndMapToCategorizedFilters,
+ type HotelResponse,
+} from "./helpers"
import HotelCount from "./HotelCount"
import HotelSorter from "./HotelSorter"
import { MapWithButtonWrapper } from "./MapWithButtonWrapper"
@@ -35,7 +38,7 @@ interface SelectHotelProps {
topSlot?: ReactNode
}
-export function SelectHotel({
+export async function SelectHotel({
bookingCode,
city,
hotels,
@@ -61,7 +64,11 @@ export function SelectHotel({
const showBookingCodeFilter = isBookingCodeRateAvailable && !isSpecialRate
- const filterList = getFiltersFromHotels(hotels, showBookingCodeFilter)
+ const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
+ hotels,
+ showBookingCodeFilter,
+ lang
+ )
return (
<>
diff --git a/packages/booking-flow/lib/pages/AlternativeHotelsMapPage.tsx b/packages/booking-flow/lib/pages/AlternativeHotelsMapPage.tsx
index 12a617031..6b131ce93 100644
--- a/packages/booking-flow/lib/pages/AlternativeHotelsMapPage.tsx
+++ b/packages/booking-flow/lib/pages/AlternativeHotelsMapPage.tsx
@@ -8,7 +8,7 @@ import { env } from "../../env/server"
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
import { MapContainer } from "../components/MapContainer"
import {
- getFiltersFromHotels,
+ fetchHotelFiltersAndMapToCategorizedFilters,
getHotels,
} from "../components/SelectHotel/helpers"
import {
@@ -108,7 +108,11 @@ export async function AlternativeHotelsMapPage({
config,
})
- const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable)
+ const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
+ hotels,
+ isBookingCodeRateAvailable,
+ lang
+ )
return (
diff --git a/packages/booking-flow/lib/pages/SelectHotelMapPage.tsx b/packages/booking-flow/lib/pages/SelectHotelMapPage.tsx
index bc8c8b92d..5c65eacca 100644
--- a/packages/booking-flow/lib/pages/SelectHotelMapPage.tsx
+++ b/packages/booking-flow/lib/pages/SelectHotelMapPage.tsx
@@ -9,7 +9,7 @@ import { env } from "../../env/server"
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
import { MapContainer } from "../components/MapContainer"
import {
- getFiltersFromHotels,
+ fetchHotelFiltersAndMapToCategorizedFilters,
getHotels,
} from "../components/SelectHotel/helpers"
import {
@@ -109,7 +109,11 @@ export async function SelectHotelMapPage({
config,
})
- const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable)
+ const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
+ hotels,
+ isBookingCodeRateAvailable,
+ lang
+ )
const suspenseKey = stringify(searchParams)
diff --git a/packages/booking-flow/lib/types.ts b/packages/booking-flow/lib/types.ts
index 2127c1858..420531584 100644
--- a/packages/booking-flow/lib/types.ts
+++ b/packages/booking-flow/lib/types.ts
@@ -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 HotelFilter = Hotel["detailedFacilities"][number] & {
+export type SelectHotelFilter = HotelFilter & {
hotelId: string
hotelIds: string[]
bookingCodeFilteredIds: string[]
}
export type CategorizedHotelFilters = {
- facilityFilters: HotelFilter[]
- surroundingsFilters: HotelFilter[]
+ facilityFilters: SelectHotelFilter[]
+ surroundingsFilters: SelectHotelFilter[]
}
diff --git a/packages/common/constants/country.ts b/packages/common/constants/country.ts
index 3ccdd1e4d..3cf819436 100644
--- a/packages/common/constants/country.ts
+++ b/packages/common/constants/country.ts
@@ -6,3 +6,12 @@ export enum Country {
Poland = "Poland",
Sweden = "Sweden",
}
+
+export const CountryCode: Record = {
+ [Country.Denmark]: "DK",
+ [Country.Finland]: "FI",
+ [Country.Germany]: "DE",
+ [Country.Norway]: "NO",
+ [Country.Poland]: "PL",
+ [Country.Sweden]: "SE",
+}
diff --git a/packages/trpc/lib/graphql/Fragments/HotelFilter.graphql.ts b/packages/trpc/lib/graphql/Fragments/HotelFilter.graphql.ts
index 2af30abca..536e51a11 100644
--- a/packages/trpc/lib/graphql/Fragments/HotelFilter.graphql.ts
+++ b/packages/trpc/lib/graphql/Fragments/HotelFilter.graphql.ts
@@ -8,8 +8,8 @@ export const HotelFilter = gql`
facility_id
category
slug
+ sort_order
}
- ${System}
`
export const HotelFilterRef = gql`
diff --git a/packages/trpc/lib/graphql/Query/HotelFilters.graphql.ts b/packages/trpc/lib/graphql/Query/HotelFilters.graphql.ts
new file mode 100644
index 000000000..b5f88eac3
--- /dev/null
+++ b/packages/trpc/lib/graphql/Query/HotelFilters.graphql.ts
@@ -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}
+`
diff --git a/packages/trpc/lib/routers/contentstack/metadata/utils.ts b/packages/trpc/lib/routers/contentstack/metadata/utils.ts
index d428225fe..c85b4859d 100644
--- a/packages/trpc/lib/routers/contentstack/metadata/utils.ts
+++ b/packages/trpc/lib/routers/contentstack/metadata/utils.ts
@@ -1,16 +1,13 @@
import { ApiCountry } from "../../../types/country"
import { HotelSortOption } from "../../../types/hotel"
-import {
- getFiltersFromHotels,
- mergeHotelFiltersAndSeoFilters,
-} from "../../../utils/getFiltersFromHotels"
import { getSortedCities } from "../../../utils/getSortedCities"
+import { mergeHotelFiltersAndSeoFilters } from "../../../utils/mergeHotelFiltersAndSeoFilters"
+import { getHotelFilters } from "../../hotels/filters/utils"
import {
getCityByCityIdentifier,
getHotelIdsByCityIdentifier,
} from "../../hotels/services/getCityByCityIdentifier"
import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry"
-import { getHotelsByHotelIds } from "../../hotels/services/getHotelsByHotelIds"
import { getCityPages } from "../destinationCountryPage/utils"
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
@@ -62,11 +59,9 @@ export async function getCityData(
serviceToken
)
- const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
-
let filterType
if (filter) {
- const hotelFilters = getFiltersFromHotels(hotels, lang)
+ const hotelFilters = await getHotelFilters(lang)
const allFilters = mergeHotelFiltersAndSeoFilters(
hotelFilters,
seoFilters
@@ -104,6 +99,7 @@ export async function getCountryData(
lang: Lang
) {
const country = data.destination_settings?.country
+ const seoFilters = transformDestinationFiltersResponse(data.seo_filters)
const filter = input.filterFromUrl
if (country) {
@@ -117,10 +113,13 @@ export async function getCountryData(
serviceToken,
})
- const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
-
if (filter) {
- const allFilters = getFiltersFromHotels(hotels, lang)
+ const hotelFilters = await getHotelFilters(lang)
+ const allFilters = mergeHotelFiltersAndSeoFilters(
+ hotelFilters,
+ seoFilters
+ )
+
const facilityFilter = allFilters.facilityFilters.find(
(f) => f.slug === filter
)
diff --git a/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts b/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts
index efd0c3973..ebd573e85 100644
--- a/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts
+++ b/packages/trpc/lib/routers/contentstack/schemas/destinationFilters.ts
@@ -1,10 +1,10 @@
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 { hotelFilterSchema } from "../../hotels/filters/output"
import { accordionSchema } from "./blocks/accordion"
import { contentSchema } from "./blocks/content"
import { systemSchema } from "./system"
@@ -37,12 +37,7 @@ export const destinationFilterSchema = z.object({
filterConnection: z.object({
edges: z.array(
z.object({
- node: z.object({
- title: z.string(),
- facility_id: z.nativeEnum(FacilityEnum).catch(FacilityEnum.UNKNOWN),
- category: z.string(),
- slug: z.string(),
- }),
+ node: hotelFilterSchema,
})
),
}),
@@ -84,13 +79,7 @@ export function transformDestinationFiltersResponse(
heading,
preamble,
blocks,
- filter: {
- id: filter.facility_id,
- name: filter.title,
- filterType: filter.category,
- slug: filter.slug,
- sortOrder: 0,
- },
+ filter,
}
})
.filter(isDefined)
diff --git a/packages/trpc/lib/routers/hotels/filters/get.ts b/packages/trpc/lib/routers/hotels/filters/get.ts
new file mode 100644
index 000000000..5fb6aa522
--- /dev/null
+++ b/packages/trpc/lib/routers/hotels/filters/get.ts
@@ -0,0 +1,6 @@
+import { contentstackBaseProcedure } from "../../../procedures"
+import { getHotelFilters } from "./utils"
+
+export const get = contentstackBaseProcedure.query(async ({ ctx }) => {
+ return getHotelFilters(ctx.lang)
+})
diff --git a/packages/trpc/lib/routers/hotels/filters/index.ts b/packages/trpc/lib/routers/hotels/filters/index.ts
new file mode 100644
index 000000000..ec53b8024
--- /dev/null
+++ b/packages/trpc/lib/routers/hotels/filters/index.ts
@@ -0,0 +1,6 @@
+import { router } from "../../.."
+import { get } from "./get"
+
+export const filtersRouter = router({
+ get,
+})
diff --git a/packages/trpc/lib/routers/hotels/filters/output.ts b/packages/trpc/lib/routers/hotels/filters/output.ts
new file mode 100644
index 000000000..7d5d0ba4d
--- /dev/null
+++ b/packages/trpc/lib/routers/hotels/filters/output.ts
@@ -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
+export type HotelFilter = HotelFilters[keyof HotelFilters][number]
diff --git a/packages/trpc/lib/routers/hotels/filters/utils.ts b/packages/trpc/lib/routers/hotels/filters/utils.ts
new file mode 100644
index 000000000..5c509944b
--- /dev/null
+++ b/packages/trpc/lib/routers/hotels/filters/utils.ts
@@ -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(
+ 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
+ })
+}
diff --git a/packages/trpc/lib/routers/hotels/query.ts b/packages/trpc/lib/routers/hotels/query.ts
index d2d9bff11..6ef3fee2d 100644
--- a/packages/trpc/lib/routers/hotels/query.ts
+++ b/packages/trpc/lib/routers/hotels/query.ts
@@ -47,6 +47,7 @@ import { getHotelsByHotelIds } from "./services/getHotelsByHotelIds"
import { getLocationsByCountries } from "./services/getLocationsByCountries"
import { getPackages } from "./services/getPackages"
import { availability } from "./availability"
+import { filtersRouter } from "./filters"
import { locationsRouter } from "./locations"
import type { HotelListingHotelData } from "../../types/hotel"
@@ -331,6 +332,7 @@ export const hotelQueryRouter = router({
env.CACHE_TIME_HOTELS
)
}),
+ filters: filtersRouter,
locations: locationsRouter,
map: router({
city: serviceProcedure
diff --git a/packages/trpc/lib/types/hotel.ts b/packages/trpc/lib/types/hotel.ts
index b8f97a5ae..3b179a0e7 100644
--- a/packages/trpc/lib/types/hotel.ts
+++ b/packages/trpc/lib/types/hotel.ts
@@ -1,4 +1,3 @@
-import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
import type { z } from "zod"
import type {
@@ -87,20 +86,6 @@ export type RoomType = Pick
export type RewardNight = z.output
-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 {
Recommended = "recommended",
Distance = "distance",
diff --git a/packages/trpc/lib/utils/getFiltersFromHotels.ts b/packages/trpc/lib/utils/getFiltersFromHotels.ts
deleted file mode 100644
index 48d639b9c..000000000
--- a/packages/trpc/lib/utils/getFiltersFromHotels.ts
+++ /dev/null
@@ -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()
- 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
-}
diff --git a/packages/trpc/lib/utils/mergeHotelFiltersAndSeoFilters.ts b/packages/trpc/lib/utils/mergeHotelFiltersAndSeoFilters.ts
new file mode 100644
index 000000000..0016b87ed
--- /dev/null
+++ b/packages/trpc/lib/utils/mergeHotelFiltersAndSeoFilters.ts
@@ -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()
+ 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,
+ }
+}