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:
@@ -4,12 +4,13 @@ import {
|
||||
type HotelSortItem,
|
||||
HotelSortOption,
|
||||
} 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 { getLang } from "@/i18n/serverContext"
|
||||
import HotelListingDataProvider from "@/providers/HotelListingDataProvider"
|
||||
|
||||
import CampaignHotelListingSkeleton from "./CampaignHotelListingSkeleton"
|
||||
@@ -35,14 +36,13 @@ export default async function CampaignHotelListing({
|
||||
isMainBlock = false,
|
||||
}: CampaignHotelListingProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = await getLang()
|
||||
const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds })
|
||||
|
||||
if (!hotels.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const allFilters = getFiltersFromHotels(hotels, lang)
|
||||
const allFilters = await getHotelFilters()
|
||||
const sortItems: HotelSortItem[] = [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
type HotelSortItem,
|
||||
HotelSortOption,
|
||||
} from "@scandic-hotels/trpc/types/hotel"
|
||||
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
getDestinationCityPage,
|
||||
getHotelFilters,
|
||||
getHotelsByCityIdentifier,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
@@ -17,7 +17,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
||||
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||
import { getPathname } from "@/utils/getPathname"
|
||||
|
||||
@@ -45,7 +44,6 @@ export default async function DestinationCityPage({
|
||||
filterFromUrl,
|
||||
}: DestinationCityPageProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = await getLang()
|
||||
const pathname = await getPathname()
|
||||
const pageData = await getDestinationCityPage()
|
||||
|
||||
@@ -70,7 +68,7 @@ export default async function DestinationCityPage({
|
||||
const activeSeoFilter = getActiveSeoFilter(seo_filters, filterFromUrl)
|
||||
|
||||
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
|
||||
const hotelFilters = getFiltersFromHotels(allHotels, lang)
|
||||
const allHotelFilters = await getHotelFilters()
|
||||
|
||||
const sortItems: HotelSortItem[] = [
|
||||
{
|
||||
@@ -102,7 +100,7 @@ export default async function DestinationCityPage({
|
||||
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
||||
<DestinationDataProvider
|
||||
allHotels={allHotels}
|
||||
hotelFilters={hotelFilters}
|
||||
hotelFilters={allHotelFilters}
|
||||
seoFilters={seo_filters}
|
||||
sortItems={sortItems}
|
||||
pathname={pathname}
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
type HotelSortItem,
|
||||
HotelSortOption,
|
||||
} from "@scandic-hotels/trpc/types/hotel"
|
||||
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
getDestinationCityPagesByCountry,
|
||||
getDestinationCountryPage,
|
||||
getHotelFilters,
|
||||
getHotelsByCountry,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
@@ -18,7 +18,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
||||
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||
import { getPathname } from "@/utils/getPathname"
|
||||
|
||||
@@ -46,7 +45,6 @@ export default async function DestinationCountryPage({
|
||||
filterFromUrl,
|
||||
}: DestinationCountryPageProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = await getLang()
|
||||
const pathname = await getPathname()
|
||||
const pageData = await getDestinationCountryPage()
|
||||
|
||||
@@ -74,7 +72,7 @@ export default async function DestinationCountryPage({
|
||||
getHotelsByCountry(destination_settings.country),
|
||||
getDestinationCityPagesByCountry(destination_settings.country),
|
||||
])
|
||||
const hotelFilters = getFiltersFromHotels(allHotels, lang)
|
||||
const allHotelFilters = await getHotelFilters()
|
||||
|
||||
const sortItems: HotelSortItem[] = [
|
||||
{
|
||||
@@ -114,7 +112,7 @@ export default async function DestinationCountryPage({
|
||||
<DestinationDataProvider
|
||||
allHotels={allHotels}
|
||||
allCities={allCitiesWithCount}
|
||||
hotelFilters={hotelFilters}
|
||||
hotelFilters={allHotelFilters}
|
||||
seoFilters={seo_filters}
|
||||
sortItems={sortItems}
|
||||
pathname={pathname}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CheckboxProps {
|
||||
name: string
|
||||
value: string
|
||||
isSelected: boolean
|
||||
isDisabled?: boolean
|
||||
onChange: (filterId: string) => void
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ export default function Checkbox({
|
||||
isSelected,
|
||||
name,
|
||||
value,
|
||||
isDisabled,
|
||||
onChange,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
@@ -25,6 +27,7 @@ export default function Checkbox({
|
||||
className={styles.checkboxWrapper}
|
||||
isSelected={isSelected}
|
||||
onChange={() => onChange(value)}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
|
||||
@@ -10,13 +10,17 @@ import Checkbox from "./Checkbox"
|
||||
|
||||
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 {
|
||||
filters: CategorizedHotelFilters
|
||||
filters: HotelFilters
|
||||
listType: "city" | "hotel"
|
||||
}
|
||||
|
||||
export default function Filter({ filters }: FilterProps) {
|
||||
export default function Filter({ filters, listType }: FilterProps) {
|
||||
const intl = useIntl()
|
||||
const { facilityFilters, surroundingsFilters } = filters
|
||||
const { pendingFilters, togglePendingFilter } = useDestinationDataStore(
|
||||
@@ -54,8 +58,9 @@ export default function Filter({ filters }: FilterProps) {
|
||||
{facilityFilters.map((filter) => (
|
||||
<li key={`filter-${filter.slug}`}>
|
||||
<Checkbox
|
||||
name={filter.name}
|
||||
name={getCheckboxLabelWithCount(filter, listType)}
|
||||
value={filter.slug}
|
||||
isDisabled={filter.hotelCount === 0}
|
||||
onChange={() => togglePendingFilter(filter)}
|
||||
isSelected={
|
||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||
@@ -79,8 +84,9 @@ export default function Filter({ filters }: FilterProps) {
|
||||
{surroundingsFilters.map((filter) => (
|
||||
<li key={`filter-${filter.slug}`}>
|
||||
<Checkbox
|
||||
name={filter.name}
|
||||
name={getCheckboxLabelWithCount(filter, listType)}
|
||||
value={filter.slug}
|
||||
isDisabled={filter.hotelCount === 0}
|
||||
onChange={() => togglePendingFilter(filter)}
|
||||
isSelected={
|
||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||
@@ -94,3 +100,17 @@ export default function Filter({ filters }: FilterProps) {
|
||||
</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
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function DestinationFilterAndSort({
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const {
|
||||
allFilters,
|
||||
filtersWithCount,
|
||||
sortItems,
|
||||
pendingFilters,
|
||||
pendingSort,
|
||||
@@ -47,7 +47,7 @@ export default function DestinationFilterAndSort({
|
||||
resetPendingValues,
|
||||
setIsLoading,
|
||||
} = useDestinationDataStore((state) => ({
|
||||
allFilters: state.allFilters,
|
||||
filtersWithCount: state.filtersWithCount,
|
||||
sortItems: state.sortItems,
|
||||
pendingFilters: state.pendingFilters,
|
||||
pendingSort: state.pendingSort,
|
||||
@@ -166,7 +166,7 @@ export default function DestinationFilterAndSort({
|
||||
<div className={styles.content}>
|
||||
<Sort sortItems={sortItems} />
|
||||
<Divider className={styles.divider} />
|
||||
<Filter filters={allFilters} />
|
||||
<Filter filters={filtersWithCount} listType={listType} />
|
||||
</div>
|
||||
{pendingCount === 0 && (
|
||||
<div className={styles.alertWrapper}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<AriaCheckbox
|
||||
className={styles.checkboxWrapper}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={() => onChange(value)}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
|
||||
@@ -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) => (
|
||||
<li key={`filter-${filter.slug}`}>
|
||||
<Checkbox
|
||||
name={filter.name}
|
||||
name={
|
||||
filter.hotelCount
|
||||
? `${filter.name} (${filter.hotelCount})`
|
||||
: filter.name
|
||||
}
|
||||
isDisabled={!filter.hotelCount}
|
||||
value={filter.slug}
|
||||
onChange={() => togglePendingFilter(filter.slug)}
|
||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
||||
onChange={() => togglePendingFilter(filter)}
|
||||
isSelected={
|
||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -81,10 +88,17 @@ export default function Filter({ filters }: FilterProps) {
|
||||
{facilityFilters.map((filter) => (
|
||||
<li key={`filter-${filter.slug}`}>
|
||||
<Checkbox
|
||||
name={filter.name}
|
||||
name={
|
||||
filter.hotelCount
|
||||
? `${filter.name} (${filter.hotelCount})`
|
||||
: filter.name
|
||||
}
|
||||
isDisabled={!filter.hotelCount}
|
||||
value={filter.slug}
|
||||
onChange={() => togglePendingFilter(filter.slug)}
|
||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
||||
onChange={() => togglePendingFilter(filter)}
|
||||
isSelected={
|
||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -104,10 +118,17 @@ export default function Filter({ filters }: FilterProps) {
|
||||
{surroundingsFilters.map((filter) => (
|
||||
<li key={`filter-${filter.slug}`}>
|
||||
<Checkbox
|
||||
name={filter.name}
|
||||
name={
|
||||
filter.hotelCount
|
||||
? `${filter.name} (${filter.hotelCount})`
|
||||
: filter.name
|
||||
}
|
||||
isDisabled={!filter.hotelCount}
|
||||
value={filter.slug}
|
||||
onChange={() => togglePendingFilter(filter.slug)}
|
||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
||||
onChange={() => togglePendingFilter(filter)}
|
||||
isSelected={
|
||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -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() {
|
||||
<div className={styles.content}>
|
||||
<Sort sortItems={sortItems} />
|
||||
<Divider className={styles.divider} />
|
||||
<Filter filters={filters} />
|
||||
<Filter filters={filtersWithCount} />
|
||||
</div>
|
||||
{pendingCount === 0 && (
|
||||
<div className={styles.alertWrapper}>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = [],
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<HotelFilter[]>()
|
||||
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<DestinationDataState>((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,
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<HotelListingDataState>((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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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<DestinationDataState, "allHotels" | "allCities" | "sortItems"> {
|
||||
export interface InitialState extends Pick<
|
||||
DestinationDataState,
|
||||
"allHotels" | "allCities" | "sortItems"
|
||||
> {
|
||||
pathname: string
|
||||
searchParams: ReadonlyURLSearchParams
|
||||
hotelFilters: CategorizedHotelFilters
|
||||
hotelFilters: HotelFilters
|
||||
seoFilters: DestinationFilters
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CategorizedHotelFilters> {
|
||||
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) =>
|
||||
<HotelFilter>{
|
||||
...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<CategorizedHotelFilters>((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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 (
|
||||
<BookingFlowConfig config={config}>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -6,3 +6,12 @@ export enum Country {
|
||||
Poland = "Poland",
|
||||
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",
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export const HotelFilter = gql`
|
||||
facility_id
|
||||
category
|
||||
slug
|
||||
sort_order
|
||||
}
|
||||
${System}
|
||||
`
|
||||
|
||||
export const HotelFilterRef = gql`
|
||||
|
||||
14
packages/trpc/lib/graphql/Query/HotelFilters.graphql.ts
Normal file
14
packages/trpc/lib/graphql/Query/HotelFilters.graphql.ts
Normal 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}
|
||||
`
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
6
packages/trpc/lib/routers/hotels/filters/get.ts
Normal file
6
packages/trpc/lib/routers/hotels/filters/get.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { contentstackBaseProcedure } from "../../../procedures"
|
||||
import { getHotelFilters } from "./utils"
|
||||
|
||||
export const get = contentstackBaseProcedure.query(async ({ ctx }) => {
|
||||
return getHotelFilters(ctx.lang)
|
||||
})
|
||||
6
packages/trpc/lib/routers/hotels/filters/index.ts
Normal file
6
packages/trpc/lib/routers/hotels/filters/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { router } from "../../.."
|
||||
import { get } from "./get"
|
||||
|
||||
export const filtersRouter = router({
|
||||
get,
|
||||
})
|
||||
61
packages/trpc/lib/routers/hotels/filters/output.ts
Normal file
61
packages/trpc/lib/routers/hotels/filters/output.ts
Normal 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]
|
||||
98
packages/trpc/lib/routers/hotels/filters/utils.ts
Normal file
98
packages/trpc/lib/routers/hotels/filters/utils.ts
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<Room, "roomTypes" | "name">
|
||||
|
||||
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 {
|
||||
Recommended = "recommended",
|
||||
Distance = "distance",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
43
packages/trpc/lib/utils/mergeHotelFiltersAndSeoFilters.ts
Normal file
43
packages/trpc/lib/utils/mergeHotelFiltersAndSeoFilters.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user