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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user