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,
|
type HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
|
||||||
|
|
||||||
import { getHotelsByCSFilter } from "@/lib/trpc/memoizedRequests"
|
import {
|
||||||
|
getHotelFilters,
|
||||||
|
getHotelsByCSFilter,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
import HotelListingDataProvider from "@/providers/HotelListingDataProvider"
|
import HotelListingDataProvider from "@/providers/HotelListingDataProvider"
|
||||||
|
|
||||||
import CampaignHotelListingSkeleton from "./CampaignHotelListingSkeleton"
|
import CampaignHotelListingSkeleton from "./CampaignHotelListingSkeleton"
|
||||||
@@ -35,14 +36,13 @@ export default async function CampaignHotelListing({
|
|||||||
isMainBlock = false,
|
isMainBlock = false,
|
||||||
}: CampaignHotelListingProps) {
|
}: CampaignHotelListingProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
|
||||||
const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds })
|
const hotels = await getHotelsByCSFilter({ hotelsToInclude: hotelIds })
|
||||||
|
|
||||||
if (!hotels.length) {
|
if (!hotels.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const allFilters = getFiltersFromHotels(hotels, lang)
|
const allFilters = await getHotelFilters()
|
||||||
const sortItems: HotelSortItem[] = [
|
const sortItems: HotelSortItem[] = [
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
type HotelSortItem,
|
type HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import {
|
import {
|
||||||
getDestinationCityPage,
|
getDestinationCityPage,
|
||||||
|
getHotelFilters,
|
||||||
getHotelsByCityIdentifier,
|
getHotelsByCityIdentifier,
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
|||||||
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||||
import { getPathname } from "@/utils/getPathname"
|
import { getPathname } from "@/utils/getPathname"
|
||||||
|
|
||||||
@@ -45,7 +44,6 @@ export default async function DestinationCityPage({
|
|||||||
filterFromUrl,
|
filterFromUrl,
|
||||||
}: DestinationCityPageProps) {
|
}: DestinationCityPageProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
|
||||||
const pathname = await getPathname()
|
const pathname = await getPathname()
|
||||||
const pageData = await getDestinationCityPage()
|
const pageData = await getDestinationCityPage()
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ export default async function DestinationCityPage({
|
|||||||
const activeSeoFilter = getActiveSeoFilter(seo_filters, filterFromUrl)
|
const activeSeoFilter = getActiveSeoFilter(seo_filters, filterFromUrl)
|
||||||
|
|
||||||
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
|
const allHotels = await getHotelsByCityIdentifier(cityIdentifier)
|
||||||
const hotelFilters = getFiltersFromHotels(allHotels, lang)
|
const allHotelFilters = await getHotelFilters()
|
||||||
|
|
||||||
const sortItems: HotelSortItem[] = [
|
const sortItems: HotelSortItem[] = [
|
||||||
{
|
{
|
||||||
@@ -102,7 +100,7 @@ export default async function DestinationCityPage({
|
|||||||
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
<Suspense fallback={<DestinationCityPageSkeleton />}>
|
||||||
<DestinationDataProvider
|
<DestinationDataProvider
|
||||||
allHotels={allHotels}
|
allHotels={allHotels}
|
||||||
hotelFilters={hotelFilters}
|
hotelFilters={allHotelFilters}
|
||||||
seoFilters={seo_filters}
|
seoFilters={seo_filters}
|
||||||
sortItems={sortItems}
|
sortItems={sortItems}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
type HotelSortItem,
|
type HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
import { getFiltersFromHotels } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import {
|
import {
|
||||||
getDestinationCityPagesByCountry,
|
getDestinationCityPagesByCountry,
|
||||||
getDestinationCountryPage,
|
getDestinationCountryPage,
|
||||||
|
getHotelFilters,
|
||||||
getHotelsByCountry,
|
getHotelsByCountry,
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
|||||||
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
import { SeoFilters } from "@/components/ContentType/DestinationPage/SeoFilters"
|
||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
|
||||||
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
import DestinationDataProvider from "@/providers/DestinationDataProvider"
|
||||||
import { getPathname } from "@/utils/getPathname"
|
import { getPathname } from "@/utils/getPathname"
|
||||||
|
|
||||||
@@ -46,7 +45,6 @@ export default async function DestinationCountryPage({
|
|||||||
filterFromUrl,
|
filterFromUrl,
|
||||||
}: DestinationCountryPageProps) {
|
}: DestinationCountryPageProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
|
||||||
const pathname = await getPathname()
|
const pathname = await getPathname()
|
||||||
const pageData = await getDestinationCountryPage()
|
const pageData = await getDestinationCountryPage()
|
||||||
|
|
||||||
@@ -74,7 +72,7 @@ export default async function DestinationCountryPage({
|
|||||||
getHotelsByCountry(destination_settings.country),
|
getHotelsByCountry(destination_settings.country),
|
||||||
getDestinationCityPagesByCountry(destination_settings.country),
|
getDestinationCityPagesByCountry(destination_settings.country),
|
||||||
])
|
])
|
||||||
const hotelFilters = getFiltersFromHotels(allHotels, lang)
|
const allHotelFilters = await getHotelFilters()
|
||||||
|
|
||||||
const sortItems: HotelSortItem[] = [
|
const sortItems: HotelSortItem[] = [
|
||||||
{
|
{
|
||||||
@@ -114,7 +112,7 @@ export default async function DestinationCountryPage({
|
|||||||
<DestinationDataProvider
|
<DestinationDataProvider
|
||||||
allHotels={allHotels}
|
allHotels={allHotels}
|
||||||
allCities={allCitiesWithCount}
|
allCities={allCitiesWithCount}
|
||||||
hotelFilters={hotelFilters}
|
hotelFilters={allHotelFilters}
|
||||||
seoFilters={seo_filters}
|
seoFilters={seo_filters}
|
||||||
sortItems={sortItems}
|
sortItems={sortItems}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
|
|||||||
@@ -6,36 +6,38 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--Corner-radius-md);
|
border-radius: var(--Corner-radius-md);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper:hover {
|
@media (hover: hover) {
|
||||||
background-color: var(--UI-Input-Controls-Surface-Hover);
|
&:not([data-disabled]):hover {
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected] .checkbox {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--Surface-UI-Fill-Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
border-color: var(--UI-Input-Controls-Border-Disabled);
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
|
background: var(--UI-Input-Controls-Surface-Normal);
|
||||||
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
||||||
border-radius: var(--Corner-radius-sm);
|
border-radius: 4px;
|
||||||
transition: all 0.3s;
|
transition: all 200ms;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
forced-color-adjust: none;
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper[data-selected] .checkbox {
|
|
||||||
border-color: var(--UI-Input-Controls-Fill-Selected);
|
|
||||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
|
||||||
.checkboxWrapper:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper[data-selected] {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface CheckboxProps {
|
|||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
onChange: (filterId: string) => void
|
onChange: (filterId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ export default function Checkbox({
|
|||||||
isSelected,
|
isSelected,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
isDisabled,
|
||||||
onChange,
|
onChange,
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +27,7 @@ export default function Checkbox({
|
|||||||
className={styles.checkboxWrapper}
|
className={styles.checkboxWrapper}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onChange={() => onChange(value)}
|
onChange={() => onChange(value)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import Checkbox from "./Checkbox"
|
|||||||
|
|
||||||
import styles from "./filter.module.css"
|
import styles from "./filter.module.css"
|
||||||
|
|
||||||
import type { CategorizedHotelFilters } from "@scandic-hotels/trpc/types/hotel"
|
import type {
|
||||||
|
HotelFilter,
|
||||||
|
HotelFilters,
|
||||||
|
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
|
||||||
interface FilterProps {
|
interface FilterProps {
|
||||||
filters: CategorizedHotelFilters
|
filters: HotelFilters
|
||||||
|
listType: "city" | "hotel"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Filter({ filters }: FilterProps) {
|
export default function Filter({ filters, listType }: FilterProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { facilityFilters, surroundingsFilters } = filters
|
const { facilityFilters, surroundingsFilters } = filters
|
||||||
const { pendingFilters, togglePendingFilter } = useDestinationDataStore(
|
const { pendingFilters, togglePendingFilter } = useDestinationDataStore(
|
||||||
@@ -54,8 +58,9 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
{facilityFilters.map((filter) => (
|
{facilityFilters.map((filter) => (
|
||||||
<li key={`filter-${filter.slug}`}>
|
<li key={`filter-${filter.slug}`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={getCheckboxLabelWithCount(filter, listType)}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
|
isDisabled={filter.hotelCount === 0}
|
||||||
onChange={() => togglePendingFilter(filter)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={
|
isSelected={
|
||||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
@@ -79,8 +84,9 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
{surroundingsFilters.map((filter) => (
|
{surroundingsFilters.map((filter) => (
|
||||||
<li key={`filter-${filter.slug}`}>
|
<li key={`filter-${filter.slug}`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={getCheckboxLabelWithCount(filter, listType)}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
|
isDisabled={filter.hotelCount === 0}
|
||||||
onChange={() => togglePendingFilter(filter)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={
|
isSelected={
|
||||||
!!pendingFilters.find((pf) => pf.id === filter.id)
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
@@ -94,3 +100,17 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCheckboxLabelWithCount(
|
||||||
|
filter: HotelFilter,
|
||||||
|
listType: "city" | "hotel"
|
||||||
|
) {
|
||||||
|
if (listType === "city" && filter.cityCount) {
|
||||||
|
return `${filter.name} (${filter.cityCount})`
|
||||||
|
}
|
||||||
|
if (listType === "hotel" && filter.hotelCount) {
|
||||||
|
return `${filter.name} (${filter.hotelCount})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter.name
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function DestinationFilterAndSort({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {
|
const {
|
||||||
allFilters,
|
filtersWithCount,
|
||||||
sortItems,
|
sortItems,
|
||||||
pendingFilters,
|
pendingFilters,
|
||||||
pendingSort,
|
pendingSort,
|
||||||
@@ -47,7 +47,7 @@ export default function DestinationFilterAndSort({
|
|||||||
resetPendingValues,
|
resetPendingValues,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
} = useDestinationDataStore((state) => ({
|
} = useDestinationDataStore((state) => ({
|
||||||
allFilters: state.allFilters,
|
filtersWithCount: state.filtersWithCount,
|
||||||
sortItems: state.sortItems,
|
sortItems: state.sortItems,
|
||||||
pendingFilters: state.pendingFilters,
|
pendingFilters: state.pendingFilters,
|
||||||
pendingSort: state.pendingSort,
|
pendingSort: state.pendingSort,
|
||||||
@@ -166,7 +166,7 @@ export default function DestinationFilterAndSort({
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Sort sortItems={sortItems} />
|
<Sort sortItems={sortItems} />
|
||||||
<Divider className={styles.divider} />
|
<Divider className={styles.divider} />
|
||||||
<Filter filters={allFilters} />
|
<Filter filters={filtersWithCount} listType={listType} />
|
||||||
</div>
|
</div>
|
||||||
{pendingCount === 0 && (
|
{pendingCount === 0 && (
|
||||||
<div className={styles.alertWrapper}>
|
<div className={styles.alertWrapper}>
|
||||||
|
|||||||
@@ -6,36 +6,38 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--Corner-radius-md);
|
border-radius: var(--Corner-radius-md);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper:hover {
|
@media (hover: hover) {
|
||||||
background-color: var(--UI-Input-Controls-Surface-Hover);
|
&:not([data-disabled]):hover {
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-selected] .checkbox {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--Surface-UI-Fill-Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
border-color: var(--UI-Input-Controls-Border-Disabled);
|
||||||
|
background-color: var(--UI-Input-Controls-Surface-Disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
|
background: var(--UI-Input-Controls-Surface-Normal);
|
||||||
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
||||||
border-radius: var(--Corner-radius-sm);
|
border-radius: 4px;
|
||||||
transition: all 0.3s;
|
transition: all 200ms;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--UI-Input-Controls-Surface-Normal);
|
forced-color-adjust: none;
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper[data-selected] .checkbox {
|
|
||||||
border-color: var(--UI-Input-Controls-Fill-Selected);
|
|
||||||
background-color: var(--UI-Input-Controls-Fill-Selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
|
||||||
.checkboxWrapper:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkboxWrapper[data-selected] {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface CheckboxProps {
|
|||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
isDisabled?: boolean
|
||||||
onChange: (filterId: string) => void
|
onChange: (filterId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,12 +19,14 @@ export default function Checkbox({
|
|||||||
isSelected,
|
isSelected,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
isDisabled,
|
||||||
onChange,
|
onChange,
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<AriaCheckbox
|
<AriaCheckbox
|
||||||
className={styles.checkboxWrapper}
|
className={styles.checkboxWrapper}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
onChange={() => onChange(value)}
|
onChange={() => onChange(value)}
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import Checkbox from "./Checkbox"
|
|||||||
|
|
||||||
import styles from "./filter.module.css"
|
import styles from "./filter.module.css"
|
||||||
|
|
||||||
import type { CategorizedHotelFilters } from "@scandic-hotels/trpc/types/hotel"
|
import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
|
||||||
interface FilterProps {
|
interface FilterProps {
|
||||||
filters: CategorizedHotelFilters
|
filters: HotelFilters
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Filter({ filters }: FilterProps) {
|
export default function Filter({ filters }: FilterProps) {
|
||||||
@@ -58,10 +58,17 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
{countryFilters.map((filter) => (
|
{countryFilters.map((filter) => (
|
||||||
<li key={`filter-${filter.slug}`}>
|
<li key={`filter-${filter.slug}`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={
|
||||||
|
filter.hotelCount
|
||||||
|
? `${filter.name} (${filter.hotelCount})`
|
||||||
|
: filter.name
|
||||||
|
}
|
||||||
|
isDisabled={!filter.hotelCount}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
onChange={() => togglePendingFilter(filter.slug)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
isSelected={
|
||||||
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -81,10 +88,17 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
{facilityFilters.map((filter) => (
|
{facilityFilters.map((filter) => (
|
||||||
<li key={`filter-${filter.slug}`}>
|
<li key={`filter-${filter.slug}`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={
|
||||||
|
filter.hotelCount
|
||||||
|
? `${filter.name} (${filter.hotelCount})`
|
||||||
|
: filter.name
|
||||||
|
}
|
||||||
|
isDisabled={!filter.hotelCount}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
onChange={() => togglePendingFilter(filter.slug)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
isSelected={
|
||||||
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -104,10 +118,17 @@ export default function Filter({ filters }: FilterProps) {
|
|||||||
{surroundingsFilters.map((filter) => (
|
{surroundingsFilters.map((filter) => (
|
||||||
<li key={`filter-${filter.slug}`}>
|
<li key={`filter-${filter.slug}`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={filter.name}
|
name={
|
||||||
|
filter.hotelCount
|
||||||
|
? `${filter.name} (${filter.hotelCount})`
|
||||||
|
: filter.name
|
||||||
|
}
|
||||||
|
isDisabled={!filter.hotelCount}
|
||||||
value={filter.slug}
|
value={filter.slug}
|
||||||
onChange={() => togglePendingFilter(filter.slug)}
|
onChange={() => togglePendingFilter(filter)}
|
||||||
isSelected={!!pendingFilters.find((f) => f === filter.slug)}
|
isSelected={
|
||||||
|
!!pendingFilters.find((pf) => pf.id === filter.id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function HotelFilterAndSort() {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {
|
const {
|
||||||
filters,
|
filtersWithCount,
|
||||||
sortItems,
|
sortItems,
|
||||||
pendingFilters,
|
pendingFilters,
|
||||||
pendingSort,
|
pendingSort,
|
||||||
@@ -39,7 +39,7 @@ export default function HotelFilterAndSort() {
|
|||||||
resetPendingValues,
|
resetPendingValues,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
} = useHotelListingDataStore((state) => ({
|
} = useHotelListingDataStore((state) => ({
|
||||||
filters: state.allFilters,
|
filtersWithCount: state.filtersWithCount,
|
||||||
sortItems: state.sortItems,
|
sortItems: state.sortItems,
|
||||||
pendingFilters: state.pendingFilters,
|
pendingFilters: state.pendingFilters,
|
||||||
pendingSort: state.pendingSort,
|
pendingSort: state.pendingSort,
|
||||||
@@ -135,7 +135,7 @@ export default function HotelFilterAndSort() {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Sort sortItems={sortItems} />
|
<Sort sortItems={sortItems} />
|
||||||
<Divider className={styles.divider} />
|
<Divider className={styles.divider} />
|
||||||
<Filter filters={filters} />
|
<Filter filters={filtersWithCount} />
|
||||||
</div>
|
</div>
|
||||||
{pendingCount === 0 && (
|
{pendingCount === 0 && (
|
||||||
<div className={styles.alertWrapper}>
|
<div className={styles.alertWrapper}>
|
||||||
|
|||||||
@@ -203,6 +203,12 @@ export const getHotelsByCityIdentifier = cache(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const getHotelFilters = cache(async function getMemoizedHotelFilters() {
|
||||||
|
const caller = await serverClient()
|
||||||
|
return caller.hotel.filters.get()
|
||||||
|
})
|
||||||
|
|
||||||
export const getAllHotelData = cache(async function getMemoizedAllHotelData() {
|
export const getAllHotelData = cache(async function getMemoizedAllHotelData() {
|
||||||
const caller = await serverClient()
|
const caller = await serverClient()
|
||||||
return caller.hotel.hotels.getAllHotelData()
|
return caller.hotel.hotels.getAllHotelData()
|
||||||
|
|||||||
@@ -8,8 +8,25 @@ import { DestinationDataContext } from "@/contexts/DestinationData"
|
|||||||
|
|
||||||
import DestinationDataProviderContent from "./Content"
|
import DestinationDataProviderContent from "./Content"
|
||||||
|
|
||||||
|
import type { HotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
||||||
|
import type { DestinationFilters } from "@scandic-hotels/trpc/types/destinationsData"
|
||||||
|
import type {
|
||||||
|
HotelListingHotelData,
|
||||||
|
HotelSortItem,
|
||||||
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
|
|
||||||
import type { DestinationDataStore } from "@/types/contexts/destination-data"
|
import type { DestinationDataStore } from "@/types/contexts/destination-data"
|
||||||
import type { DestinationDataProviderProps } from "@/types/providers/destination-data"
|
|
||||||
|
interface DestinationDataProviderProps extends React.PropsWithChildren {
|
||||||
|
allHotels: HotelListingHotelData[]
|
||||||
|
allCities?: DestinationCityListItem[]
|
||||||
|
hotelFilters: HotelFilters
|
||||||
|
seoFilters: DestinationFilters
|
||||||
|
filterFromUrl?: string
|
||||||
|
sortItems: HotelSortItem[]
|
||||||
|
pathname: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function DestinationDataProvider({
|
export default function DestinationDataProvider({
|
||||||
allCities = [],
|
allCities = [],
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
type HotelFilter,
|
|
||||||
type HotelListingHotelData,
|
type HotelListingHotelData,
|
||||||
type HotelSortItem,
|
type HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
HotelFilter,
|
||||||
|
HotelFilters,
|
||||||
|
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
||||||
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
|
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
|
||||||
|
|
||||||
@@ -32,7 +35,9 @@ export function getFilteredHotels(
|
|||||||
if (filters.length) {
|
if (filters.length) {
|
||||||
return hotels.filter(({ hotel }) =>
|
return hotels.filter(({ hotel }) =>
|
||||||
filters.every((filter) =>
|
filters.every((filter) =>
|
||||||
hotel.detailedFacilities.some((facility) => facility.id === filter.id)
|
hotel.detailedFacilities.some(
|
||||||
|
(facility) => facility.id === Number(filter.id)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,3 +101,81 @@ export function getActiveDestinationFilter(
|
|||||||
}
|
}
|
||||||
return allSeoFilters.find((f) => f.filter.id === filterFromUrl.id) || null
|
return allSeoFilters.find((f) => f.filter.id === filterFromUrl.id) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFiltersWithHotelAndCityCount(
|
||||||
|
hotelFilters: HotelFilters,
|
||||||
|
hotels: HotelListingHotelData[],
|
||||||
|
cities: DestinationCityListItem[]
|
||||||
|
): HotelFilters {
|
||||||
|
const flattenedFilters = Object.values(hotelFilters).flat()
|
||||||
|
const filterHotelCounts: Record<string, number> = {}
|
||||||
|
const filterCityIdentifiers: Record<string, Set<string>> = {}
|
||||||
|
|
||||||
|
hotels.forEach(({ hotel }) => {
|
||||||
|
hotel.detailedFacilities.forEach((facility) => {
|
||||||
|
filterHotelCounts[facility.id] = (filterHotelCounts[facility.id] || 0) + 1
|
||||||
|
|
||||||
|
if (!filterCityIdentifiers[facility.id]) {
|
||||||
|
filterCityIdentifiers[facility.id] = new Set()
|
||||||
|
}
|
||||||
|
if (hotel.cityIdentifier) {
|
||||||
|
filterCityIdentifiers[facility.id].add(hotel.cityIdentifier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hotel.countryCode) {
|
||||||
|
filterHotelCounts[hotel.countryCode] =
|
||||||
|
(filterHotelCounts[hotel.countryCode] || 0) + 1
|
||||||
|
|
||||||
|
if (!filterCityIdentifiers[hotel.countryCode]) {
|
||||||
|
filterCityIdentifiers[hotel.countryCode] = new Set()
|
||||||
|
}
|
||||||
|
if (hotel.cityIdentifier) {
|
||||||
|
filterCityIdentifiers[hotel.countryCode].add(hotel.cityIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Count cities that match the cityIdentifiers for each filter
|
||||||
|
const filterCityCounts: Record<string, number> = {}
|
||||||
|
Object.entries(filterCityIdentifiers).forEach(
|
||||||
|
([filterId, cityIdentifiers]) => {
|
||||||
|
filterCityCounts[filterId] = cities.filter(
|
||||||
|
(city) =>
|
||||||
|
city.destination_settings.city &&
|
||||||
|
cityIdentifiers.has(city.destination_settings.city)
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return flattenedFilters.reduce(
|
||||||
|
(acc, filter) => {
|
||||||
|
if (filter.filterType === "facility") {
|
||||||
|
acc.facilityFilters.push({
|
||||||
|
...filter,
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
cityCount: filterCityCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
} else if (filter.filterType === "surroundings") {
|
||||||
|
acc.surroundingsFilters.push({
|
||||||
|
...filter,
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
cityCount: filterCityCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
} else if (filter.filterType === "country") {
|
||||||
|
acc.countryFilters.push({
|
||||||
|
...filter,
|
||||||
|
filterType: "country",
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
cityCount: filterCityCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{
|
||||||
|
facilityFilters: [],
|
||||||
|
surroundingsFilters: [],
|
||||||
|
countryFilters: [],
|
||||||
|
} as HotelFilters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { produce } from "immer"
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/getFiltersFromHotels"
|
|
||||||
import { getSortedCities } from "@scandic-hotels/trpc/utils/getSortedCities"
|
import { getSortedCities } from "@scandic-hotels/trpc/utils/getSortedCities"
|
||||||
|
import { mergeHotelFiltersAndSeoFilters } from "@scandic-hotels/trpc/utils/mergeHotelFiltersAndSeoFilters"
|
||||||
|
|
||||||
import { DestinationDataContext } from "@/contexts/DestinationData"
|
import { DestinationDataContext } from "@/contexts/DestinationData"
|
||||||
import {
|
import {
|
||||||
@@ -16,12 +16,13 @@ import {
|
|||||||
getBasePathNameWithoutFilters,
|
getBasePathNameWithoutFilters,
|
||||||
getFilteredCities,
|
getFilteredCities,
|
||||||
getFilteredHotels,
|
getFilteredHotels,
|
||||||
|
getFiltersWithHotelAndCityCount,
|
||||||
getSortedHotels,
|
getSortedHotels,
|
||||||
isValidSortOption,
|
isValidSortOption,
|
||||||
} from "./helper"
|
} from "./helper"
|
||||||
|
|
||||||
|
import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
|
import type { DestinationFilter } from "@scandic-hotels/trpc/types/destinationsData"
|
||||||
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
DestinationDataState,
|
DestinationDataState,
|
||||||
@@ -39,9 +40,10 @@ export function createDestinationDataStore({
|
|||||||
}: InitialState) {
|
}: InitialState) {
|
||||||
const defaultSort =
|
const defaultSort =
|
||||||
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
||||||
|
|
||||||
const allFilters = mergeHotelFiltersAndSeoFilters(hotelFilters, seoFilters)
|
const allFilters = mergeHotelFiltersAndSeoFilters(hotelFilters, seoFilters)
|
||||||
const allSeoFilters = Object.values(seoFilters).flat()
|
const allSeoFilters = Object.values(seoFilters).flat()
|
||||||
const allFlattenedFilters = Object.values(allFilters).flat<HotelFilter[]>()
|
const allFlattenedFilters = Object.values(allFilters).flat()
|
||||||
const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
|
const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
|
||||||
const activeFilters: HotelFilter[] = []
|
const activeFilters: HotelFilter[] = []
|
||||||
let activeSeoFilter: DestinationFilter | null = null
|
let activeSeoFilter: DestinationFilter | null = null
|
||||||
@@ -75,6 +77,12 @@ export function createDestinationDataStore({
|
|||||||
const filteredCities = getFilteredCities(filteredHotels, allCities)
|
const filteredCities = getFilteredCities(filteredHotels, allCities)
|
||||||
const activeCities = getSortedCities(filteredCities, activeSort)
|
const activeCities = getSortedCities(filteredCities, activeSort)
|
||||||
|
|
||||||
|
const filtersWithCount = getFiltersWithHotelAndCityCount(
|
||||||
|
allFilters,
|
||||||
|
filteredHotels,
|
||||||
|
filteredCities
|
||||||
|
)
|
||||||
|
|
||||||
return create<DestinationDataState>((set) => ({
|
return create<DestinationDataState>((set) => ({
|
||||||
actions: {
|
actions: {
|
||||||
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
|
updateActiveFiltersAndSort(filterSlugs, sort, filterSlugFromUrl) {
|
||||||
@@ -175,9 +183,16 @@ export function createDestinationDataStore({
|
|||||||
? getFilteredCities(pendingHotels, state.allCities)
|
? getFilteredCities(pendingHotels, state.allCities)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
|
const pendingFiltersWithCount = getFiltersWithHotelAndCityCount(
|
||||||
|
state.allFilters,
|
||||||
|
pendingHotels,
|
||||||
|
pendingCities
|
||||||
|
)
|
||||||
|
|
||||||
state.pendingFilters = filters
|
state.pendingFilters = filters
|
||||||
state.pendingHotelCount = pendingHotels.length
|
state.pendingHotelCount = pendingHotels.length
|
||||||
state.pendingCityCount = pendingCities.length
|
state.pendingCityCount = pendingCities.length
|
||||||
|
state.filtersWithCount = pendingFiltersWithCount
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -187,6 +202,11 @@ export function createDestinationDataStore({
|
|||||||
state.pendingFilters = []
|
state.pendingFilters = []
|
||||||
state.pendingHotelCount = state.allHotels.length
|
state.pendingHotelCount = state.allHotels.length
|
||||||
state.pendingCityCount = state.allCities.length
|
state.pendingCityCount = state.allCities.length
|
||||||
|
state.filtersWithCount = getFiltersWithHotelAndCityCount(
|
||||||
|
state.allFilters,
|
||||||
|
state.allHotels,
|
||||||
|
state.allCities
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -197,6 +217,19 @@ export function createDestinationDataStore({
|
|||||||
state.pendingSort = state.activeSort
|
state.pendingSort = state.activeSort
|
||||||
state.pendingHotelCount = state.activeHotels.length
|
state.pendingHotelCount = state.activeHotels.length
|
||||||
state.pendingCityCount = state.activeCities.length
|
state.pendingCityCount = state.activeCities.length
|
||||||
|
const filteredHotels = getFilteredHotels(
|
||||||
|
state.allHotels,
|
||||||
|
state.activeFilters
|
||||||
|
)
|
||||||
|
const filteredCities = getFilteredCities(
|
||||||
|
filteredHotels,
|
||||||
|
state.allCities
|
||||||
|
)
|
||||||
|
state.filtersWithCount = getFiltersWithHotelAndCityCount(
|
||||||
|
state.allFilters,
|
||||||
|
filteredHotels,
|
||||||
|
filteredCities
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -213,6 +246,7 @@ export function createDestinationDataStore({
|
|||||||
activeFilters,
|
activeFilters,
|
||||||
pendingFilters: activeFilters,
|
pendingFilters: activeFilters,
|
||||||
allFilters,
|
allFilters,
|
||||||
|
filtersWithCount,
|
||||||
activeSeoFilter,
|
activeSeoFilter,
|
||||||
filterFromUrl,
|
filterFromUrl,
|
||||||
basePathnameWithoutFilters,
|
basePathnameWithoutFilters,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import {
|
|||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
HotelFilter,
|
||||||
|
HotelFilters,
|
||||||
|
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
|
||||||
const HOTEL_SORTING_STRATEGIES: Partial<
|
const HOTEL_SORTING_STRATEGIES: Partial<
|
||||||
Record<
|
Record<
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
@@ -20,16 +25,15 @@ const HOTEL_SORTING_STRATEGIES: Partial<
|
|||||||
|
|
||||||
export function getFilteredHotels(
|
export function getFilteredHotels(
|
||||||
hotels: HotelListingHotelData[],
|
hotels: HotelListingHotelData[],
|
||||||
filters: string[]
|
filters: HotelFilter[]
|
||||||
) {
|
) {
|
||||||
if (filters.length) {
|
if (filters.length) {
|
||||||
return hotels.filter(({ hotel }) =>
|
return hotels.filter(({ hotel }) =>
|
||||||
filters.every((filter) => {
|
filters.every((filter) => {
|
||||||
const matchesFacility = hotel.detailedFacilities.some(
|
const matchesFacility = hotel.detailedFacilities.some(
|
||||||
(facility) => facility.slug === filter
|
(facility) => facility.id === Number(filter.id)
|
||||||
)
|
)
|
||||||
const matchesCountry =
|
const matchesCountry = hotel.countryCode === filter.id
|
||||||
hotel.countryCode.toLowerCase() === filter.toLowerCase()
|
|
||||||
return matchesFacility || matchesCountry
|
return matchesFacility || matchesCountry
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -51,3 +55,50 @@ export function isValidSortOption(
|
|||||||
): value is HotelSortOption {
|
): value is HotelSortOption {
|
||||||
return sortItems.map((item) => item.value).includes(value as HotelSortOption)
|
return sortItems.map((item) => item.value).includes(value as HotelSortOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFiltersWithHotelCount(
|
||||||
|
hotelFilters: HotelFilters,
|
||||||
|
hotels: HotelListingHotelData[]
|
||||||
|
): HotelFilters {
|
||||||
|
const flattenedFilters = Object.values(hotelFilters).flat()
|
||||||
|
const filterHotelCounts: Record<string, number> = {}
|
||||||
|
|
||||||
|
hotels.forEach(({ hotel }) => {
|
||||||
|
hotel.detailedFacilities.forEach((facility) => {
|
||||||
|
filterHotelCounts[facility.id] = (filterHotelCounts[facility.id] || 0) + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hotel.countryCode) {
|
||||||
|
filterHotelCounts[hotel.countryCode] =
|
||||||
|
(filterHotelCounts[hotel.countryCode] || 0) + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return flattenedFilters.reduce(
|
||||||
|
(acc, filter) => {
|
||||||
|
if (filter.filterType === "facility") {
|
||||||
|
acc.facilityFilters.push({
|
||||||
|
...filter,
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
} else if (filter.filterType === "surroundings") {
|
||||||
|
acc.surroundingsFilters.push({
|
||||||
|
...filter,
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
} else if (filter.filterType === "country") {
|
||||||
|
acc.countryFilters.push({
|
||||||
|
...filter,
|
||||||
|
filterType: "country",
|
||||||
|
hotelCount: filterHotelCounts[filter.id] ?? 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{
|
||||||
|
facilityFilters: [],
|
||||||
|
surroundingsFilters: [],
|
||||||
|
countryFilters: [],
|
||||||
|
} as HotelFilters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ import {
|
|||||||
trackSortingChangeEvent,
|
trackSortingChangeEvent,
|
||||||
} from "@/utils/tracking/destinationPage"
|
} from "@/utils/tracking/destinationPage"
|
||||||
|
|
||||||
import { getFilteredHotels, getSortedHotels, isValidSortOption } from "./helper"
|
import {
|
||||||
|
getFilteredHotels,
|
||||||
|
getFiltersWithHotelCount,
|
||||||
|
getSortedHotels,
|
||||||
|
isValidSortOption,
|
||||||
|
} from "./helper"
|
||||||
|
|
||||||
import type { HotelFilter } from "@scandic-hotels/trpc/types/hotel"
|
import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
HotelListingDataState,
|
HotelListingDataState,
|
||||||
@@ -25,11 +30,10 @@ export function createHotelListingDataStore({
|
|||||||
}: InitialState) {
|
}: InitialState) {
|
||||||
const defaultSort =
|
const defaultSort =
|
||||||
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
sortItems.find((s) => s.isDefault)?.value ?? sortItems[0].value
|
||||||
const allFilterSlugs = Object.values(allFilters).flatMap(
|
const allFlattenedFilters = Object.values(allFilters).flat()
|
||||||
(filter: HotelFilter[]) => filter.map((f) => f.slug)
|
const allFilterSlugs = allFlattenedFilters.map((filter) => filter.slug)
|
||||||
)
|
|
||||||
|
|
||||||
const activeFilters: string[] = []
|
const activeFilters: HotelFilter[] = []
|
||||||
|
|
||||||
let activeSort = defaultSort
|
let activeSort = defaultSort
|
||||||
if (searchParams) {
|
if (searchParams) {
|
||||||
@@ -43,8 +47,9 @@ export function createHotelListingDataStore({
|
|||||||
if (filterParam) {
|
if (filterParam) {
|
||||||
const filters = filterParam.split(",")
|
const filters = filterParam.split(",")
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
if (allFilterSlugs.includes(filter)) {
|
const filterFromUrl = allFlattenedFilters.find((f) => f.slug === filter)
|
||||||
activeFilters.push(filter)
|
if (filterFromUrl) {
|
||||||
|
activeFilters.push(filterFromUrl)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,15 +57,20 @@ export function createHotelListingDataStore({
|
|||||||
const filteredHotels = getFilteredHotels(allHotels, activeFilters)
|
const filteredHotels = getFilteredHotels(allHotels, activeFilters)
|
||||||
const activeHotels = getSortedHotels(filteredHotels, activeSort)
|
const activeHotels = getSortedHotels(filteredHotels, activeSort)
|
||||||
|
|
||||||
|
const filtersWithCount = getFiltersWithHotelCount(allFilters, filteredHotels)
|
||||||
|
|
||||||
return create<HotelListingDataState>((set) => ({
|
return create<HotelListingDataState>((set) => ({
|
||||||
actions: {
|
actions: {
|
||||||
updateActiveFiltersAndSort(filters, sort) {
|
updateActiveFiltersAndSort(filterSlugs, sort) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: HotelListingDataState) => {
|
produce((state: HotelListingDataState) => {
|
||||||
const newSort =
|
const newSort =
|
||||||
sort && isValidSortOption(sort, state.sortItems)
|
sort && isValidSortOption(sort, state.sortItems)
|
||||||
? sort
|
? sort
|
||||||
: state.defaultSort
|
: state.defaultSort
|
||||||
|
const filters = allFlattenedFilters.filter((filter) =>
|
||||||
|
filterSlugs.includes(filter.slug)
|
||||||
|
)
|
||||||
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
const filteredHotels = getFilteredHotels(state.allHotels, filters)
|
||||||
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
const sortedHotels = getSortedHotels(filteredHotels, newSort)
|
||||||
|
|
||||||
@@ -71,16 +81,22 @@ export function createHotelListingDataStore({
|
|||||||
if (
|
if (
|
||||||
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
|
JSON.stringify(filters) !== JSON.stringify(state.activeFilters)
|
||||||
) {
|
) {
|
||||||
const facilityFiltersUsed = filters.filter((f) =>
|
const facilityFiltersUsed = filters
|
||||||
state.allFilters.facilityFilters
|
.filter(
|
||||||
.map((ff) => ff.slug)
|
(f) =>
|
||||||
.includes(f)
|
!!state.allFilters.facilityFilters.find(
|
||||||
)
|
(ff) => ff.id === f.id
|
||||||
const surroundingsFiltersUsed = filters.filter((f) =>
|
)
|
||||||
state.allFilters.surroundingsFilters
|
)
|
||||||
.map((sf) => sf.slug)
|
.map((f) => f.slug)
|
||||||
.includes(f)
|
const surroundingsFiltersUsed = filters
|
||||||
)
|
.filter(
|
||||||
|
(f) =>
|
||||||
|
!!state.allFilters.surroundingsFilters.find(
|
||||||
|
(sf) => sf.id === f.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((f) => f.slug)
|
||||||
|
|
||||||
trackFilterChangeEvent(
|
trackFilterChangeEvent(
|
||||||
facilityFiltersUsed,
|
facilityFiltersUsed,
|
||||||
@@ -116,14 +132,23 @@ export function createHotelListingDataStore({
|
|||||||
togglePendingFilter(filter) {
|
togglePendingFilter(filter) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: HotelListingDataState) => {
|
produce((state: HotelListingDataState) => {
|
||||||
const isActive = state.pendingFilters.includes(filter)
|
const filterId = filter.id
|
||||||
|
const isActive = !!state.pendingFilters.find(
|
||||||
|
(pf) => pf.id === filterId
|
||||||
|
)
|
||||||
const filters = isActive
|
const filters = isActive
|
||||||
? state.pendingFilters.filter((f) => f !== filter)
|
? state.pendingFilters.filter((pf) => pf.id !== filterId)
|
||||||
: [...state.pendingFilters, filter]
|
: [...state.pendingFilters, filter]
|
||||||
const pendingHotels = getFilteredHotels(state.allHotels, filters)
|
const pendingHotels = getFilteredHotels(state.allHotels, filters)
|
||||||
|
|
||||||
|
const pendingFiltersWithCount = getFiltersWithHotelCount(
|
||||||
|
state.allFilters,
|
||||||
|
pendingHotels
|
||||||
|
)
|
||||||
|
|
||||||
state.pendingFilters = filters
|
state.pendingFilters = filters
|
||||||
state.pendingHotelCount = pendingHotels.length
|
state.pendingHotelCount = pendingHotels.length
|
||||||
|
state.filtersWithCount = pendingFiltersWithCount
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -132,6 +157,10 @@ export function createHotelListingDataStore({
|
|||||||
produce((state: HotelListingDataState) => {
|
produce((state: HotelListingDataState) => {
|
||||||
state.pendingFilters = []
|
state.pendingFilters = []
|
||||||
state.pendingHotelCount = state.allHotels.length
|
state.pendingHotelCount = state.allHotels.length
|
||||||
|
state.filtersWithCount = getFiltersWithHotelCount(
|
||||||
|
state.allFilters,
|
||||||
|
state.allHotels
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -141,6 +170,10 @@ export function createHotelListingDataStore({
|
|||||||
state.pendingFilters = state.activeFilters
|
state.pendingFilters = state.activeFilters
|
||||||
state.pendingSort = state.activeSort
|
state.pendingSort = state.activeSort
|
||||||
state.pendingHotelCount = state.activeHotels.length
|
state.pendingHotelCount = state.activeHotels.length
|
||||||
|
state.filtersWithCount = getFiltersWithHotelCount(
|
||||||
|
state.allFilters,
|
||||||
|
state.activeHotels
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -154,6 +187,7 @@ export function createHotelListingDataStore({
|
|||||||
activeFilters,
|
activeFilters,
|
||||||
pendingFilters: activeFilters,
|
pendingFilters: activeFilters,
|
||||||
allFilters,
|
allFilters,
|
||||||
|
filtersWithCount,
|
||||||
allFilterSlugs,
|
allFilterSlugs,
|
||||||
sortItems,
|
sortItems,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -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 {
|
import type {
|
||||||
CategorizedHotelFilters,
|
|
||||||
HotelListingHotelData,
|
HotelListingHotelData,
|
||||||
HotelSortItem,
|
HotelSortItem,
|
||||||
} from "@scandic-hotels/trpc/types/hotel"
|
} from "@scandic-hotels/trpc/types/hotel"
|
||||||
|
|
||||||
export interface HotelListingDataProviderProps extends React.PropsWithChildren {
|
export interface HotelListingDataProviderProps extends React.PropsWithChildren {
|
||||||
allHotels: HotelListingHotelData[]
|
allHotels: HotelListingHotelData[]
|
||||||
allFilters: CategorizedHotelFilters
|
allFilters: HotelFilters
|
||||||
sortItems: HotelSortItem[]
|
sortItems: HotelSortItem[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import type {
|
||||||
|
HotelFilter,
|
||||||
|
HotelFilters,
|
||||||
|
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
import type { DestinationCityListItem } from "@scandic-hotels/trpc/types/destinationCityPage"
|
||||||
import type {
|
import type {
|
||||||
DestinationFilter,
|
DestinationFilter,
|
||||||
DestinationFilters,
|
DestinationFilters,
|
||||||
} from "@scandic-hotels/trpc/types/destinationsData"
|
} from "@scandic-hotels/trpc/types/destinationsData"
|
||||||
import type {
|
import type {
|
||||||
CategorizedHotelFilters,
|
|
||||||
HotelFilter,
|
|
||||||
HotelListingHotelData,
|
HotelListingHotelData,
|
||||||
HotelSortItem,
|
HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
@@ -39,17 +41,20 @@ export interface DestinationDataState {
|
|||||||
filterFromUrl: HotelFilter | null
|
filterFromUrl: HotelFilter | null
|
||||||
pendingHotelCount: number
|
pendingHotelCount: number
|
||||||
pendingCityCount: number
|
pendingCityCount: number
|
||||||
allFilters: CategorizedHotelFilters
|
allFilters: HotelFilters
|
||||||
|
filtersWithCount: HotelFilters
|
||||||
activeSeoFilter: DestinationFilter | null
|
activeSeoFilter: DestinationFilter | null
|
||||||
basePathnameWithoutFilters: string
|
basePathnameWithoutFilters: string
|
||||||
sortItems: HotelSortItem[]
|
sortItems: HotelSortItem[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialState
|
export interface InitialState extends Pick<
|
||||||
extends Pick<DestinationDataState, "allHotels" | "allCities" | "sortItems"> {
|
DestinationDataState,
|
||||||
|
"allHotels" | "allCities" | "sortItems"
|
||||||
|
> {
|
||||||
pathname: string
|
pathname: string
|
||||||
searchParams: ReadonlyURLSearchParams
|
searchParams: ReadonlyURLSearchParams
|
||||||
hotelFilters: CategorizedHotelFilters
|
hotelFilters: HotelFilters
|
||||||
seoFilters: DestinationFilters
|
seoFilters: DestinationFilters
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
CategorizedHotelFilters,
|
HotelFilter,
|
||||||
|
HotelFilters,
|
||||||
|
} from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
import type {
|
||||||
HotelListingHotelData,
|
HotelListingHotelData,
|
||||||
HotelSortItem,
|
HotelSortItem,
|
||||||
HotelSortOption,
|
HotelSortOption,
|
||||||
@@ -9,7 +12,7 @@ import type { ReadonlyURLSearchParams } from "next/navigation"
|
|||||||
interface Actions {
|
interface Actions {
|
||||||
updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void
|
updateActiveFiltersAndSort: (filters: string[], sort: string | null) => void
|
||||||
setPendingSort: (sort: HotelSortOption) => void
|
setPendingSort: (sort: HotelSortOption) => void
|
||||||
togglePendingFilter: (filter: string) => void
|
togglePendingFilter: (filter: HotelFilter) => void
|
||||||
clearPendingFilters: () => void
|
clearPendingFilters: () => void
|
||||||
resetPendingValues: () => void
|
resetPendingValues: () => void
|
||||||
setIsLoading: (isLoading: boolean) => void
|
setIsLoading: (isLoading: boolean) => void
|
||||||
@@ -22,19 +25,19 @@ export interface HotelListingDataState {
|
|||||||
pendingSort: HotelSortOption
|
pendingSort: HotelSortOption
|
||||||
activeSort: HotelSortOption
|
activeSort: HotelSortOption
|
||||||
defaultSort: HotelSortOption
|
defaultSort: HotelSortOption
|
||||||
pendingFilters: string[]
|
pendingFilters: HotelFilter[]
|
||||||
activeFilters: string[]
|
activeFilters: HotelFilter[]
|
||||||
pendingHotelCount: number
|
pendingHotelCount: number
|
||||||
allFilters: CategorizedHotelFilters
|
allFilters: HotelFilters
|
||||||
|
filtersWithCount: HotelFilters
|
||||||
allFilterSlugs: string[]
|
allFilterSlugs: string[]
|
||||||
sortItems: HotelSortItem[]
|
sortItems: HotelSortItem[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialState
|
export interface InitialState extends Pick<
|
||||||
extends Pick<
|
HotelListingDataState,
|
||||||
HotelListingDataState,
|
"allHotels" | "sortItems" | "allFilters"
|
||||||
"allHotels" | "sortItems" | "allFilters"
|
> {
|
||||||
> {
|
|
||||||
searchParams: ReadonlyURLSearchParams
|
searchParams: ReadonlyURLSearchParams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import FilterCheckbox from "./FilterCheckbox"
|
|||||||
|
|
||||||
import styles from "./filterContent.module.css"
|
import styles from "./filterContent.module.css"
|
||||||
|
|
||||||
import type { CategorizedHotelFilters, HotelFilter } from "../../../../types"
|
import type {
|
||||||
|
CategorizedHotelFilters,
|
||||||
|
SelectHotelFilter,
|
||||||
|
} from "../../../../types"
|
||||||
|
|
||||||
interface FilterContentProps {
|
interface FilterContentProps {
|
||||||
filters: CategorizedHotelFilters
|
filters: CategorizedHotelFilters
|
||||||
@@ -75,7 +78,7 @@ export default function FilterContent({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterOutput(filters: HotelFilter[]) {
|
function filterOutput(filters: SelectHotelFilter[]) {
|
||||||
return filters.map((filter) => {
|
return filters.map((filter) => {
|
||||||
const relevantIds = showOnlyBookingCodeRates
|
const relevantIds = showOnlyBookingCodeRates
|
||||||
? filter.bookingCodeFilteredIds
|
? filter.bookingCodeFilteredIds
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
|
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
|
||||||
|
import { getHotelFilters } from "@scandic-hotels/trpc/routers/hotels/filters/utils"
|
||||||
import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers"
|
import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers"
|
||||||
|
|
||||||
import { serverClient } from "../../trpc"
|
import { serverClient } from "../../trpc"
|
||||||
@@ -18,7 +19,7 @@ import type {
|
|||||||
Location,
|
Location,
|
||||||
} from "@scandic-hotels/trpc/types/locations"
|
} from "@scandic-hotels/trpc/types/locations"
|
||||||
|
|
||||||
import type { CategorizedHotelFilters, HotelFilter } from "../../types"
|
import type { CategorizedHotelFilters, SelectHotelFilter } from "../../types"
|
||||||
|
|
||||||
type AvailabilityInput = {
|
type AvailabilityInput = {
|
||||||
cityId: string
|
cityId: string
|
||||||
@@ -273,50 +274,38 @@ export async function getHotels({
|
|||||||
return hotels
|
return hotels
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotelSurroundingsFilterNames = [
|
export async function fetchHotelFiltersAndMapToCategorizedFilters(
|
||||||
"Hotel surroundings",
|
|
||||||
"Hotel omgivelser",
|
|
||||||
"Hotelumgebung",
|
|
||||||
"Hotellia lähellä",
|
|
||||||
"Hotellomgivelser",
|
|
||||||
"Omgivningar",
|
|
||||||
]
|
|
||||||
|
|
||||||
const hotelFacilitiesFilterNames = [
|
|
||||||
"Hotel facilities",
|
|
||||||
"Hotellfaciliteter",
|
|
||||||
"Hotelfaciliteter",
|
|
||||||
"Hotel faciliteter",
|
|
||||||
"Hotel-Infos",
|
|
||||||
"Hotellin palvelut",
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getFiltersFromHotels(
|
|
||||||
hotels: HotelResponse[],
|
hotels: HotelResponse[],
|
||||||
showBookingCodeFilter: boolean
|
showBookingCodeFilter: boolean,
|
||||||
): CategorizedHotelFilters {
|
lang: Lang
|
||||||
|
): Promise<CategorizedHotelFilters> {
|
||||||
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
|
const defaultFilters = { facilityFilters: [], surroundingsFilters: [] }
|
||||||
if (!hotels.length) {
|
if (!hotels.length) {
|
||||||
return defaultFilters
|
return defaultFilters
|
||||||
}
|
}
|
||||||
|
const { countryFilters, ...hotelFilters } = await getHotelFilters(lang)
|
||||||
|
const allFlattenedFilters = Object.values(hotelFilters).flat()
|
||||||
|
const filters = hotels.flatMap(({ hotel, availability }) => {
|
||||||
|
const hotelFilterData = allFlattenedFilters.map((filter) => {
|
||||||
|
const hotelHasFilter = hotel.detailedFacilities.some(
|
||||||
|
(facility) => facility.id.toString() === filter.id
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
hotelId: hotelHasFilter ? hotel.operaId : null,
|
||||||
|
hotelIds: hotelHasFilter ? [hotel.operaId] : [],
|
||||||
|
bookingCodeFilteredIds:
|
||||||
|
(availability.bookingCode || !showBookingCodeFilter) && hotelHasFilter
|
||||||
|
? [hotel.operaId]
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const filters = hotels.flatMap(({ hotel, availability }) =>
|
return hotelFilterData
|
||||||
hotel.detailedFacilities.map(
|
})
|
||||||
(facility) =>
|
|
||||||
<HotelFilter>{
|
|
||||||
...facility,
|
|
||||||
hotelId: hotel.operaId,
|
|
||||||
hotelIds: [hotel.operaId],
|
|
||||||
bookingCodeFilteredIds:
|
|
||||||
availability.bookingCode || !showBookingCodeFilter
|
|
||||||
? [hotel.operaId]
|
|
||||||
: [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
|
||||||
const filterList: HotelFilter[] = uniqueFilterIds
|
const filterList: SelectHotelFilter[] = uniqueFilterIds
|
||||||
.map((filterId) => {
|
.map((filterId) => {
|
||||||
const filter = filters.find((f) => f.id === filterId)
|
const filter = filters.find((f) => f.id === filterId)
|
||||||
|
|
||||||
@@ -324,7 +313,9 @@ export function getFiltersFromHotels(
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
const matchingFilters = filters.filter((f) => f.id === filterId)
|
const matchingFilters = filters.filter((f) => f.id === filterId)
|
||||||
|
|
||||||
filter.hotelIds = matchingFilters.map((f) => f.hotelId)
|
filter.hotelIds = matchingFilters
|
||||||
|
.map((f) => f.hotelId)
|
||||||
|
.filter((id) => id !== null)
|
||||||
filter.bookingCodeFilteredIds = [
|
filter.bookingCodeFilteredIds = [
|
||||||
...new Set(
|
...new Set(
|
||||||
matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? [])
|
matchingFilters.flatMap((f) => f.bookingCodeFilteredIds ?? [])
|
||||||
@@ -333,18 +324,17 @@ export function getFiltersFromHotels(
|
|||||||
}
|
}
|
||||||
return filter
|
return filter
|
||||||
})
|
})
|
||||||
.filter((filter): filter is HotelFilter => filter !== undefined)
|
.filter((filter): filter is SelectHotelFilter => filter !== undefined)
|
||||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||||
|
|
||||||
return filterList.reduce<CategorizedHotelFilters>((filters, filter) => {
|
const facilityFilters = filterList.filter(
|
||||||
if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) {
|
(filter) => filter.filterType === "facility"
|
||||||
filters.surroundingsFilters.push(filter)
|
)
|
||||||
}
|
const surroundingsFilters = filterList.filter(
|
||||||
|
(filter) => filter.filterType === "surroundings"
|
||||||
if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) {
|
)
|
||||||
filters.facilityFilters.push(filter)
|
return {
|
||||||
}
|
facilityFilters,
|
||||||
|
surroundingsFilters,
|
||||||
return filters
|
}
|
||||||
}, defaultFilters)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import BookingCodeFilter from "../BookingCodeFilter"
|
|||||||
import HotelCardListing from "../HotelCardListing"
|
import HotelCardListing from "../HotelCardListing"
|
||||||
import { StaticMap } from "../StaticMap"
|
import { StaticMap } from "../StaticMap"
|
||||||
import HotelFilter from "./Filters/HotelFilter"
|
import HotelFilter from "./Filters/HotelFilter"
|
||||||
import { getFiltersFromHotels, type HotelResponse } from "./helpers"
|
import {
|
||||||
|
fetchHotelFiltersAndMapToCategorizedFilters,
|
||||||
|
type HotelResponse,
|
||||||
|
} from "./helpers"
|
||||||
import HotelCount from "./HotelCount"
|
import HotelCount from "./HotelCount"
|
||||||
import HotelSorter from "./HotelSorter"
|
import HotelSorter from "./HotelSorter"
|
||||||
import { MapWithButtonWrapper } from "./MapWithButtonWrapper"
|
import { MapWithButtonWrapper } from "./MapWithButtonWrapper"
|
||||||
@@ -35,7 +38,7 @@ interface SelectHotelProps {
|
|||||||
topSlot?: ReactNode
|
topSlot?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectHotel({
|
export async function SelectHotel({
|
||||||
bookingCode,
|
bookingCode,
|
||||||
city,
|
city,
|
||||||
hotels,
|
hotels,
|
||||||
@@ -61,7 +64,11 @@ export function SelectHotel({
|
|||||||
|
|
||||||
const showBookingCodeFilter = isBookingCodeRateAvailable && !isSpecialRate
|
const showBookingCodeFilter = isBookingCodeRateAvailable && !isSpecialRate
|
||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels, showBookingCodeFilter)
|
const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
|
||||||
|
hotels,
|
||||||
|
showBookingCodeFilter,
|
||||||
|
lang
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { env } from "../../env/server"
|
|||||||
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
|
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
|
||||||
import { MapContainer } from "../components/MapContainer"
|
import { MapContainer } from "../components/MapContainer"
|
||||||
import {
|
import {
|
||||||
getFiltersFromHotels,
|
fetchHotelFiltersAndMapToCategorizedFilters,
|
||||||
getHotels,
|
getHotels,
|
||||||
} from "../components/SelectHotel/helpers"
|
} from "../components/SelectHotel/helpers"
|
||||||
import {
|
import {
|
||||||
@@ -108,7 +108,11 @@ export async function AlternativeHotelsMapPage({
|
|||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable)
|
const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
|
||||||
|
hotels,
|
||||||
|
isBookingCodeRateAvailable,
|
||||||
|
lang
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookingFlowConfig config={config}>
|
<BookingFlowConfig config={config}>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { env } from "../../env/server"
|
|||||||
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
|
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
|
||||||
import { MapContainer } from "../components/MapContainer"
|
import { MapContainer } from "../components/MapContainer"
|
||||||
import {
|
import {
|
||||||
getFiltersFromHotels,
|
fetchHotelFiltersAndMapToCategorizedFilters,
|
||||||
getHotels,
|
getHotels,
|
||||||
} from "../components/SelectHotel/helpers"
|
} from "../components/SelectHotel/helpers"
|
||||||
import {
|
import {
|
||||||
@@ -109,7 +109,11 @@ export async function SelectHotelMapPage({
|
|||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterList = getFiltersFromHotels(hotels, isBookingCodeRateAvailable)
|
const filterList = await fetchHotelFiltersAndMapToCategorizedFilters(
|
||||||
|
hotels,
|
||||||
|
isBookingCodeRateAvailable,
|
||||||
|
lang
|
||||||
|
)
|
||||||
|
|
||||||
const suspenseKey = stringify(searchParams)
|
const suspenseKey = stringify(searchParams)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
|
import type { HotelFilter } from "@scandic-hotels/trpc/routers/hotels/filters/output"
|
||||||
|
|
||||||
export type NextSearchParams = { [key: string]: string | string[] | undefined }
|
export type NextSearchParams = { [key: string]: string | string[] | undefined }
|
||||||
|
|
||||||
export type HotelFilter = Hotel["detailedFacilities"][number] & {
|
export type SelectHotelFilter = HotelFilter & {
|
||||||
hotelId: string
|
hotelId: string
|
||||||
hotelIds: string[]
|
hotelIds: string[]
|
||||||
bookingCodeFilteredIds: string[]
|
bookingCodeFilteredIds: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategorizedHotelFilters = {
|
export type CategorizedHotelFilters = {
|
||||||
facilityFilters: HotelFilter[]
|
facilityFilters: SelectHotelFilter[]
|
||||||
surroundingsFilters: HotelFilter[]
|
surroundingsFilters: SelectHotelFilter[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,12 @@ export enum Country {
|
|||||||
Poland = "Poland",
|
Poland = "Poland",
|
||||||
Sweden = "Sweden",
|
Sweden = "Sweden",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CountryCode: Record<Country, string> = {
|
||||||
|
[Country.Denmark]: "DK",
|
||||||
|
[Country.Finland]: "FI",
|
||||||
|
[Country.Germany]: "DE",
|
||||||
|
[Country.Norway]: "NO",
|
||||||
|
[Country.Poland]: "PL",
|
||||||
|
[Country.Sweden]: "SE",
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export const HotelFilter = gql`
|
|||||||
facility_id
|
facility_id
|
||||||
category
|
category
|
||||||
slug
|
slug
|
||||||
|
sort_order
|
||||||
}
|
}
|
||||||
${System}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const HotelFilterRef = gql`
|
export const HotelFilterRef = gql`
|
||||||
|
|||||||
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 { ApiCountry } from "../../../types/country"
|
||||||
import { HotelSortOption } from "../../../types/hotel"
|
import { HotelSortOption } from "../../../types/hotel"
|
||||||
import {
|
|
||||||
getFiltersFromHotels,
|
|
||||||
mergeHotelFiltersAndSeoFilters,
|
|
||||||
} from "../../../utils/getFiltersFromHotels"
|
|
||||||
import { getSortedCities } from "../../../utils/getSortedCities"
|
import { getSortedCities } from "../../../utils/getSortedCities"
|
||||||
|
import { mergeHotelFiltersAndSeoFilters } from "../../../utils/mergeHotelFiltersAndSeoFilters"
|
||||||
|
import { getHotelFilters } from "../../hotels/filters/utils"
|
||||||
import {
|
import {
|
||||||
getCityByCityIdentifier,
|
getCityByCityIdentifier,
|
||||||
getHotelIdsByCityIdentifier,
|
getHotelIdsByCityIdentifier,
|
||||||
} from "../../hotels/services/getCityByCityIdentifier"
|
} from "../../hotels/services/getCityByCityIdentifier"
|
||||||
import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry"
|
import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry"
|
||||||
import { getHotelsByHotelIds } from "../../hotels/services/getHotelsByHotelIds"
|
|
||||||
import { getCityPages } from "../destinationCountryPage/utils"
|
import { getCityPages } from "../destinationCountryPage/utils"
|
||||||
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
|
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
|
||||||
|
|
||||||
@@ -62,11 +59,9 @@ export async function getCityData(
|
|||||||
serviceToken
|
serviceToken
|
||||||
)
|
)
|
||||||
|
|
||||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
|
||||||
|
|
||||||
let filterType
|
let filterType
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const hotelFilters = getFiltersFromHotels(hotels, lang)
|
const hotelFilters = await getHotelFilters(lang)
|
||||||
const allFilters = mergeHotelFiltersAndSeoFilters(
|
const allFilters = mergeHotelFiltersAndSeoFilters(
|
||||||
hotelFilters,
|
hotelFilters,
|
||||||
seoFilters
|
seoFilters
|
||||||
@@ -104,6 +99,7 @@ export async function getCountryData(
|
|||||||
lang: Lang
|
lang: Lang
|
||||||
) {
|
) {
|
||||||
const country = data.destination_settings?.country
|
const country = data.destination_settings?.country
|
||||||
|
const seoFilters = transformDestinationFiltersResponse(data.seo_filters)
|
||||||
const filter = input.filterFromUrl
|
const filter = input.filterFromUrl
|
||||||
|
|
||||||
if (country) {
|
if (country) {
|
||||||
@@ -117,10 +113,13 @@ export async function getCountryData(
|
|||||||
serviceToken,
|
serviceToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const allFilters = getFiltersFromHotels(hotels, lang)
|
const hotelFilters = await getHotelFilters(lang)
|
||||||
|
const allFilters = mergeHotelFiltersAndSeoFilters(
|
||||||
|
hotelFilters,
|
||||||
|
seoFilters
|
||||||
|
)
|
||||||
|
|
||||||
const facilityFilter = allFilters.facilityFilters.find(
|
const facilityFilter = allFilters.facilityFilters.find(
|
||||||
(f) => f.slug === filter
|
(f) => f.slug === filter
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
|
||||||
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
|
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
|
||||||
|
|
||||||
import { DestinationFilterBlocksEnum } from "../../../types/destinationsData"
|
import { DestinationFilterBlocksEnum } from "../../../types/destinationsData"
|
||||||
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
|
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
|
||||||
|
import { hotelFilterSchema } from "../../hotels/filters/output"
|
||||||
import { accordionSchema } from "./blocks/accordion"
|
import { accordionSchema } from "./blocks/accordion"
|
||||||
import { contentSchema } from "./blocks/content"
|
import { contentSchema } from "./blocks/content"
|
||||||
import { systemSchema } from "./system"
|
import { systemSchema } from "./system"
|
||||||
@@ -37,12 +37,7 @@ export const destinationFilterSchema = z.object({
|
|||||||
filterConnection: z.object({
|
filterConnection: z.object({
|
||||||
edges: z.array(
|
edges: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
node: z.object({
|
node: hotelFilterSchema,
|
||||||
title: z.string(),
|
|
||||||
facility_id: z.nativeEnum(FacilityEnum).catch(FacilityEnum.UNKNOWN),
|
|
||||||
category: z.string(),
|
|
||||||
slug: z.string(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@@ -84,13 +79,7 @@ export function transformDestinationFiltersResponse(
|
|||||||
heading,
|
heading,
|
||||||
preamble,
|
preamble,
|
||||||
blocks,
|
blocks,
|
||||||
filter: {
|
filter,
|
||||||
id: filter.facility_id,
|
|
||||||
name: filter.title,
|
|
||||||
filterType: filter.category,
|
|
||||||
slug: filter.slug,
|
|
||||||
sortOrder: 0,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(isDefined)
|
.filter(isDefined)
|
||||||
|
|||||||
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 { getLocationsByCountries } from "./services/getLocationsByCountries"
|
||||||
import { getPackages } from "./services/getPackages"
|
import { getPackages } from "./services/getPackages"
|
||||||
import { availability } from "./availability"
|
import { availability } from "./availability"
|
||||||
|
import { filtersRouter } from "./filters"
|
||||||
import { locationsRouter } from "./locations"
|
import { locationsRouter } from "./locations"
|
||||||
|
|
||||||
import type { HotelListingHotelData } from "../../types/hotel"
|
import type { HotelListingHotelData } from "../../types/hotel"
|
||||||
@@ -331,6 +332,7 @@ export const hotelQueryRouter = router({
|
|||||||
env.CACHE_TIME_HOTELS
|
env.CACHE_TIME_HOTELS
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
filters: filtersRouter,
|
||||||
locations: locationsRouter,
|
locations: locationsRouter,
|
||||||
map: router({
|
map: router({
|
||||||
city: serviceProcedure
|
city: serviceProcedure
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { FacilityEnum } from "@scandic-hotels/common/constants/facilities"
|
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -87,20 +86,6 @@ export type RoomType = Pick<Room, "roomTypes" | "name">
|
|||||||
|
|
||||||
export type RewardNight = z.output<typeof rewardNightSchema>
|
export type RewardNight = z.output<typeof rewardNightSchema>
|
||||||
|
|
||||||
export interface HotelFilter {
|
|
||||||
id: FacilityEnum
|
|
||||||
name: string
|
|
||||||
slug: string
|
|
||||||
filterType: string
|
|
||||||
sortOrder: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CategorizedHotelFilters {
|
|
||||||
facilityFilters: HotelFilter[]
|
|
||||||
surroundingsFilters: HotelFilter[]
|
|
||||||
countryFilters: HotelFilter[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum HotelSortOption {
|
export enum HotelSortOption {
|
||||||
Recommended = "recommended",
|
Recommended = "recommended",
|
||||||
Distance = "distance",
|
Distance = "distance",
|
||||||
|
|||||||
@@ -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