Merged in fix/SW-2841-filter-popup-closing-on-select (pull request #2289)
Fix/SW-2841 filter popup closing on select * fix(SW-2841): refactored so that filter modal is not closed when selecting filters * fix(SW-2841): rename component * fix: review feedback * fix: move font-family * fix: change init value of filteredHotelIds * fix * fix: add Typography tag Approved-by: Michael Zetterberg Approved-by: Christian Andolf
This commit is contained in:
@@ -28,6 +28,7 @@ import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
|
|||||||
|
|
||||||
export default function HotelCardListing({
|
export default function HotelCardListing({
|
||||||
hotelData,
|
hotelData,
|
||||||
|
unfilteredHotelCount,
|
||||||
type = HotelCardListingTypeEnum.PageListing,
|
type = HotelCardListingTypeEnum.PageListing,
|
||||||
}: HotelCardListingProps) {
|
}: HotelCardListingProps) {
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
@@ -104,8 +105,8 @@ export default function HotelCardListing({
|
|||||||
}, [activeHotel, type])
|
}, [activeHotel, type])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setResultCount(hotels.length)
|
setResultCount(hotels.length, unfilteredHotelCount)
|
||||||
}, [hotels, setResultCount])
|
}, [hotels, setResultCount, unfilteredHotelCount])
|
||||||
|
|
||||||
function isHotelActiveInMapView(hotelName: string): boolean {
|
function isHotelActiveInMapView(hotelName: string): boolean {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
usePathname,
|
usePathname,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
} from "next/dist/client/components/navigation"
|
} from "next/dist/client/components/navigation"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import {
|
import {
|
||||||
Dialog as AriaDialog,
|
Dialog as AriaDialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -24,8 +24,8 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
||||||
|
|
||||||
import HotelFilter from "../HotelFilter"
|
import { DEFAULT_SORT } from "../../HotelSorter"
|
||||||
import { DEFAULT_SORT } from "../HotelSorter"
|
import FilterContent from "../FilterContent"
|
||||||
|
|
||||||
import styles from "./filterAndSortModal.module.css"
|
import styles from "./filterAndSortModal.module.css"
|
||||||
|
|
||||||
@@ -40,14 +40,32 @@ export default function FilterAndSortModal({
|
|||||||
setShowSkeleton,
|
setShowSkeleton,
|
||||||
}: FilterAndSortModalProps) {
|
}: FilterAndSortModalProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
useInitializeFiltersFromUrl()
|
useInitializeFiltersFromUrl()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
|
||||||
const setFilters = useHotelFilterStore((state) => state.setFilters)
|
const { resultCount, setFilters, activeFilters, unfilteredResultCount } =
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
useHotelFilterStore((state) => ({
|
||||||
|
resultCount: state.resultCount,
|
||||||
|
setFilters: state.setFilters,
|
||||||
|
activeFilters: state.activeFilters,
|
||||||
|
unfilteredResultCount: state.unfilteredResultCount,
|
||||||
|
}))
|
||||||
|
|
||||||
const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT)
|
const [sort, setSort] = useState(searchParams.get("sort") ?? DEFAULT_SORT)
|
||||||
|
|
||||||
|
const [selectedFilters, setSelectedFilters] =
|
||||||
|
useState<string[]>(activeFilters)
|
||||||
|
|
||||||
|
const [filteredCount, setFilteredCount] = useState(resultCount)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeFilters.length) {
|
||||||
|
setSelectedFilters(activeFilters)
|
||||||
|
}
|
||||||
|
}, [activeFilters])
|
||||||
|
|
||||||
const sortItems: SortItem[] = [
|
const sortItems: SortItem[] = [
|
||||||
{
|
{
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
@@ -81,14 +99,21 @@ export default function FilterAndSortModal({
|
|||||||
|
|
||||||
const handleApplyFiltersAndSorting = useCallback(
|
const handleApplyFiltersAndSorting = useCallback(
|
||||||
(close: () => void) => {
|
(close: () => void) => {
|
||||||
|
setFilters(selectedFilters)
|
||||||
|
|
||||||
if (setShowSkeleton) {
|
if (setShowSkeleton) {
|
||||||
setShowSkeleton(true)
|
setShowSkeleton(true)
|
||||||
}
|
}
|
||||||
if (sort === searchParams.get("sort")) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSearchParams = new URLSearchParams(searchParams)
|
const newSearchParams = new URLSearchParams(searchParams)
|
||||||
|
|
||||||
|
const values = selectedFilters.join(",")
|
||||||
|
if (values === "") {
|
||||||
|
newSearchParams.delete("filters")
|
||||||
|
} else {
|
||||||
|
newSearchParams.set("filters", values)
|
||||||
|
}
|
||||||
|
|
||||||
newSearchParams.set("sort", sort)
|
newSearchParams.set("sort", sort)
|
||||||
|
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
@@ -103,7 +128,7 @@ export default function FilterAndSortModal({
|
|||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pathname, searchParams, sort, setShowSkeleton]
|
[pathname, searchParams, sort, setShowSkeleton, selectedFilters, setFilters]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,7 +184,19 @@ export default function FilterAndSortModal({
|
|||||||
<Divider color="subtle" />
|
<Divider color="subtle" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<HotelFilter filters={filters} />
|
<FilterContent
|
||||||
|
filters={filters}
|
||||||
|
activeFilters={selectedFilters}
|
||||||
|
onChange={(id) => {
|
||||||
|
const isSelected = selectedFilters.includes(id)
|
||||||
|
setSelectedFilters((prev) =>
|
||||||
|
isSelected
|
||||||
|
? prev.filter((s) => s !== id)
|
||||||
|
: [...prev, id]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onFilteredCountChange={setFilteredCount}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<Button
|
<Button
|
||||||
@@ -173,12 +210,17 @@ export default function FilterAndSortModal({
|
|||||||
defaultMessage: "See results ({ count })",
|
defaultMessage: "See results ({ count })",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
count: resultCount,
|
count: filteredCount
|
||||||
|
? filteredCount
|
||||||
|
: unfilteredResultCount,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setFilters([])}
|
onClick={() => {
|
||||||
|
setSelectedFilters([])
|
||||||
|
setFilteredCount(unfilteredResultCount)
|
||||||
|
}}
|
||||||
intent="text"
|
intent="text"
|
||||||
size="medium"
|
size="medium"
|
||||||
theme="base"
|
theme="base"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.container {
|
||||||
|
min-width: 272px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities {
|
||||||
|
padding-bottom: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities:first-of-type {
|
||||||
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.facilities ul {
|
||||||
|
margin-top: var(--Space-x2);
|
||||||
|
}
|
||||||
|
.facilities:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(min-content, max-content));
|
||||||
|
gap: var(--Space-x15);
|
||||||
|
margin-bottom: var(--Space-x1);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter:first-child {
|
||||||
|
margin-top: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter input[type="checkbox"] {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
|
import FilterCheckbox from "./FilterCheckbox"
|
||||||
|
|
||||||
|
import styles from "./filterContent.module.css"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CategorizedFilters,
|
||||||
|
HotelFilter,
|
||||||
|
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||||
|
|
||||||
|
interface FilterContentProps {
|
||||||
|
filters: CategorizedFilters
|
||||||
|
activeFilters: string[]
|
||||||
|
onChange: (id: string) => void
|
||||||
|
onFilteredCountChange?: (count: number) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilterContent({
|
||||||
|
filters,
|
||||||
|
activeFilters,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
onFilteredCountChange = () => undefined,
|
||||||
|
}: FilterContentProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const [filteredHotelIds, setFilteredHotelIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeFilters.length) {
|
||||||
|
const allFilters = [
|
||||||
|
...filters.facilityFilters,
|
||||||
|
...filters.surroundingsFilters,
|
||||||
|
]
|
||||||
|
setFilteredHotelIds(
|
||||||
|
allFilters
|
||||||
|
.filter((f) => activeFilters.includes(f.id.toString()))
|
||||||
|
.map((f) => f.hotelIds)
|
||||||
|
.reduce((accumulatedHotelIds, currentHotelIds) =>
|
||||||
|
accumulatedHotelIds.filter((hotelId) =>
|
||||||
|
currentHotelIds.includes(hotelId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setFilteredHotelIds([])
|
||||||
|
}
|
||||||
|
}, [filters, activeFilters, setFilteredHotelIds])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFilteredCountChange(filteredHotelIds.length)
|
||||||
|
}, [filteredHotelIds, onFilteredCountChange])
|
||||||
|
|
||||||
|
if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterOutput(filters: HotelFilter[]) {
|
||||||
|
return filters.map((filter) => {
|
||||||
|
const isDisabled = filteredHotelIds.length
|
||||||
|
? !filter.hotelIds.some((hotelId) => filteredHotelIds.includes(hotelId))
|
||||||
|
: false
|
||||||
|
|
||||||
|
const combinedFiltersCount = filteredHotelIds.filter((id) =>
|
||||||
|
filter.hotelIds.includes(id)
|
||||||
|
).length
|
||||||
|
|
||||||
|
const filterCount = filter.hotelIds.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={filter.id} className={styles.filter}>
|
||||||
|
<FilterCheckbox
|
||||||
|
name={filter.name}
|
||||||
|
id={filter.id.toString()}
|
||||||
|
onChange={onChange}
|
||||||
|
isSelected={activeFilters.some((f) => f === filter.id.toString())}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
{!isDisabled && (
|
||||||
|
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||||
|
<span>{`(${combinedFiltersCount > 0 ? combinedFiltersCount : filterCount})`}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={`${styles.container} ${className}`}>
|
||||||
|
<div>
|
||||||
|
<Title as="h4">
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Filter by",
|
||||||
|
})}
|
||||||
|
</Title>
|
||||||
|
<div className={styles.facilities}>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Hotel facilities",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<ul>{filterOutput(filters.facilityFilters)}</ul>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.facilities}>
|
||||||
|
<Typography variant="Title/Subtitle/md">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Hotel surroundings",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<ul>{filterOutput(filters.surroundingsFilters)}</ul>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
|
import { useCallback, useEffect } from "react"
|
||||||
|
|
||||||
|
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||||
|
|
||||||
|
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
||||||
|
import { trackEvent } from "@/utils/tracking/base"
|
||||||
|
|
||||||
|
import FilterContent from "../FilterContent"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
HotelFilter,
|
||||||
|
HotelFiltersProps,
|
||||||
|
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||||
|
|
||||||
|
export default function HotelFilter({ className, filters }: HotelFiltersProps) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const pathname = usePathname()
|
||||||
|
useInitializeFiltersFromUrl()
|
||||||
|
|
||||||
|
const { toggleFilter, activeFilters } = useHotelFilterStore((state) => ({
|
||||||
|
toggleFilter: state.toggleFilter,
|
||||||
|
activeFilters: state.activeFilters,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const trackFiltersEvent = useCallback(() => {
|
||||||
|
const facilityMap = new Map(
|
||||||
|
filters.facilityFilters.map((f) => [f.id.toString(), f.name])
|
||||||
|
)
|
||||||
|
const surroundingsMap = new Map(
|
||||||
|
filters.surroundingsFilters.map((f) => [f.id.toString(), f.name])
|
||||||
|
)
|
||||||
|
|
||||||
|
const hotelFacilitiesFilter = activeFilters
|
||||||
|
.filter((id) => facilityMap.has(id))
|
||||||
|
.map((id) => facilityMap.get(id))
|
||||||
|
.join(",")
|
||||||
|
|
||||||
|
const hotelSurroundingsFilter = activeFilters
|
||||||
|
.filter((id) => surroundingsMap.has(id))
|
||||||
|
.map((id) => surroundingsMap.get(id))
|
||||||
|
.join(",")
|
||||||
|
|
||||||
|
trackEvent({
|
||||||
|
event: "filterUsed",
|
||||||
|
filter: {
|
||||||
|
filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [activeFilters, filters.facilityFilters, filters.surroundingsFilters])
|
||||||
|
|
||||||
|
// Update the URL when the filters changes
|
||||||
|
useEffect(() => {
|
||||||
|
const newSearchParams = new URLSearchParams(searchParams)
|
||||||
|
const values = activeFilters.join(",")
|
||||||
|
|
||||||
|
if (values === "") {
|
||||||
|
newSearchParams.delete("filters")
|
||||||
|
} else {
|
||||||
|
newSearchParams.set("filters", values)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values !== searchParams.get("filters")) {
|
||||||
|
if (values) {
|
||||||
|
trackFiltersEvent()
|
||||||
|
}
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
`${pathname}?${newSearchParams.toString()}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [activeFilters, pathname, searchParams, trackFiltersEvent])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterContent
|
||||||
|
className={className}
|
||||||
|
filters={filters}
|
||||||
|
activeFilters={activeFilters}
|
||||||
|
onChange={toggleFilter}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as FilterAndSortModal } from "./FilterAndSortModal"
|
||||||
|
export { default as HotelFilter } from "./HotelFilter"
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { usePathname, useSearchParams } from "next/navigation"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
|
||||||
|
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
|
||||||
import useInitializeFiltersFromUrl from "@/hooks/useInitializeFiltersFromUrl"
|
|
||||||
import { trackEvent } from "@/utils/tracking/base"
|
|
||||||
|
|
||||||
import FilterCheckbox from "./FilterCheckbox"
|
|
||||||
|
|
||||||
import styles from "./hotelFilter.module.css"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
HotelFilter,
|
|
||||||
HotelFiltersProps,
|
|
||||||
} from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
|
||||||
|
|
||||||
export default function HotelFilter({ className, filters }: HotelFiltersProps) {
|
|
||||||
const intl = useIntl()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const pathname = usePathname()
|
|
||||||
const toggleFilter = useHotelFilterStore((state) => state.toggleFilter)
|
|
||||||
useInitializeFiltersFromUrl()
|
|
||||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
|
||||||
const facilityMap = new Map(
|
|
||||||
filters.facilityFilters.map((f) => [f.id.toString(), f.name])
|
|
||||||
)
|
|
||||||
const surroundingsMap = new Map(
|
|
||||||
filters.surroundingsFilters.map((f) => [f.id.toString(), f.name])
|
|
||||||
)
|
|
||||||
|
|
||||||
function trackFiltersEvent() {
|
|
||||||
const hotelFacilitiesFilter = activeFilters
|
|
||||||
.filter((id) => facilityMap.has(id))
|
|
||||||
.map((id) => facilityMap.get(id))
|
|
||||||
.join(",")
|
|
||||||
|
|
||||||
const hotelSurroundingsFilter = activeFilters
|
|
||||||
.filter((id) => surroundingsMap.has(id))
|
|
||||||
.map((id) => surroundingsMap.get(id))
|
|
||||||
.join(",")
|
|
||||||
|
|
||||||
trackEvent({
|
|
||||||
event: "filterUsed",
|
|
||||||
filter: {
|
|
||||||
filtersUsed: `Filters values - hotelfacilities:${hotelFacilitiesFilter}|hotelsurroundings:${hotelSurroundingsFilter}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the URL when the filters changes
|
|
||||||
useEffect(() => {
|
|
||||||
const newSearchParams = new URLSearchParams(searchParams)
|
|
||||||
const values = activeFilters.join(",")
|
|
||||||
|
|
||||||
if (values === "") {
|
|
||||||
newSearchParams.delete("filters")
|
|
||||||
} else {
|
|
||||||
newSearchParams.set("filters", values)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values !== searchParams.get("filters")) {
|
|
||||||
if (values) {
|
|
||||||
trackFiltersEvent()
|
|
||||||
}
|
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
"",
|
|
||||||
`${pathname}?${newSearchParams.toString()}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [activeFilters])
|
|
||||||
|
|
||||||
if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let filteredHotelIds: string[] | undefined
|
|
||||||
if (activeFilters.length) {
|
|
||||||
const allFilters = [
|
|
||||||
...filters.facilityFilters,
|
|
||||||
...filters.surroundingsFilters,
|
|
||||||
]
|
|
||||||
filteredHotelIds = allFilters
|
|
||||||
.filter((f) => activeFilters.includes(f.id.toString()))
|
|
||||||
.map((f) => f.hotelIds)
|
|
||||||
.reduce((accumlatedHotelIds, currentHotelIds) =>
|
|
||||||
accumlatedHotelIds?.filter((hotelId) =>
|
|
||||||
currentHotelIds?.includes(hotelId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterOutput(filters: HotelFilter[]) {
|
|
||||||
return filters.map((filter) => {
|
|
||||||
const isDisabled = filteredHotelIds?.length
|
|
||||||
? !filter.hotelIds?.some((hotelId) =>
|
|
||||||
filteredHotelIds.includes(hotelId)
|
|
||||||
)
|
|
||||||
: false
|
|
||||||
return (
|
|
||||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
|
||||||
<FilterCheckbox
|
|
||||||
name={filter.name}
|
|
||||||
id={filter.id.toString()}
|
|
||||||
onChange={() => toggleFilter(filter.id.toString())}
|
|
||||||
isSelected={activeFilters.some((f) => f === filter.id.toString())}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
/>
|
|
||||||
{!isDisabled ? (
|
|
||||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
|
||||||
<span>{`(${filteredHotelIds?.filter((id) => filter.hotelIds.includes(id))?.length ?? filter.hotelIds.length})`}</span>
|
|
||||||
) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className={`${styles.container} ${className}`}>
|
|
||||||
<form>
|
|
||||||
<Title as="h4">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Filter by",
|
|
||||||
})}
|
|
||||||
</Title>
|
|
||||||
<div className={styles.facilities}>
|
|
||||||
<Subtitle>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Hotel facilities",
|
|
||||||
})}
|
|
||||||
</Subtitle>
|
|
||||||
<ul>{filterOutput(filters.facilityFilters)}</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.facilities}>
|
|
||||||
<Subtitle>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Hotel surroundings",
|
|
||||||
})}
|
|
||||||
</Subtitle>
|
|
||||||
<ul>{filterOutput(filters.surroundingsFilters)}</ul>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</aside>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ import Button from "@/components/TempDesignSystem/Button"
|
|||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import FilterAndSortModal from "../FilterAndSortModal"
|
import FilterAndSortModal from "../Filters/FilterAndSortModal"
|
||||||
|
|
||||||
import styles from "./mobileMapButtonContainer.module.css"
|
import styles from "./mobileMapButtonContainer.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import styles from "./hotelListing.module.css"
|
|||||||
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||||
import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||||
|
|
||||||
export default function HotelListing({ hotels }: HotelListingProps) {
|
export default function HotelListing({
|
||||||
|
hotels,
|
||||||
|
unfilteredHotelCount,
|
||||||
|
}: HotelListingProps) {
|
||||||
const { activeHotel } = useHotelsMapStore()
|
const { activeHotel } = useHotelsMapStore()
|
||||||
const isMobile = useMediaQuery("(max-width: 767px)")
|
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ export default function HotelListing({ hotels }: HotelListingProps) {
|
|||||||
<HotelCardListing
|
<HotelCardListing
|
||||||
hotelData={hotels}
|
hotelData={hotels}
|
||||||
type={HotelCardListingTypeEnum.MapListing}
|
type={HotelCardListingTypeEnum.MapListing}
|
||||||
|
unfilteredHotelCount={unfilteredHotelCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useScrollToTop } from "@/hooks/useScrollToTop"
|
|||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
|
|
||||||
import BookingCodeFilter from "../../BookingCodeFilter"
|
import BookingCodeFilter from "../../BookingCodeFilter"
|
||||||
import FilterAndSortModal from "../../FilterAndSortModal"
|
import FilterAndSortModal from "../../Filters/FilterAndSortModal"
|
||||||
import HotelListing from "../HotelListing"
|
import HotelListing from "../HotelListing"
|
||||||
import { getVisibleHotels } from "./utils"
|
import { getVisibleHotels } from "./utils"
|
||||||
|
|
||||||
@@ -150,6 +150,8 @@ export default function SelectHotelContent({
|
|||||||
const showBookingCodeFilter =
|
const showBookingCodeFilter =
|
||||||
bookingCode && isBookingCodeRateAvailable && !isSpecialRate
|
bookingCode && isBookingCodeRateAvailable && !isSpecialRate
|
||||||
|
|
||||||
|
const unfilteredHotelCount = hotelPins.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.listingContainer} ref={listingContainerRef}>
|
<div className={styles.listingContainer} ref={listingContainerRef}>
|
||||||
@@ -184,7 +186,10 @@ export default function SelectHotelContent({
|
|||||||
<RoomCardSkeleton />
|
<RoomCardSkeleton />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<HotelListing hotels={visibleHotels} />
|
<HotelListing
|
||||||
|
hotels={visibleHotels}
|
||||||
|
unfilteredHotelCount={unfilteredHotelCount}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{showBackToTop && (
|
{showBackToTop && (
|
||||||
<BackToTopButton position="left" onClick={scrollToTop} />
|
<BackToTopButton position="left" onClick={scrollToTop} />
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|||||||
|
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
import BookingCodeFilter from "@/components/HotelReservation/SelectHotel/BookingCodeFilter"
|
||||||
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/Filters/HotelFilter"
|
||||||
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
|
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
|
||||||
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
|
||||||
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
|
||||||
import NoAvailabilityAlert from "@/components/HotelReservation/SelectHotel/NoAvailabilityAlert"
|
import NoAvailabilityAlert from "@/components/HotelReservation/SelectHotel/NoAvailabilityAlert"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ interface HotelFilterState {
|
|||||||
toggleFilter: (filterId: string) => void
|
toggleFilter: (filterId: string) => void
|
||||||
setFilters: (filters: string[]) => void
|
setFilters: (filters: string[]) => void
|
||||||
resultCount: number
|
resultCount: number
|
||||||
setResultCount: (count: number) => void
|
unfilteredResultCount: number
|
||||||
|
setResultCount: (count: number, unfilteredCount?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHotelFilterStore = create<HotelFilterState>((set) => ({
|
export const useHotelFilterStore = create<HotelFilterState>((set) => ({
|
||||||
@@ -22,5 +23,7 @@ export const useHotelFilterStore = create<HotelFilterState>((set) => ({
|
|||||||
return { activeFilters: newFilters }
|
return { activeFilters: newFilters }
|
||||||
}),
|
}),
|
||||||
resultCount: 0,
|
resultCount: 0,
|
||||||
setResultCount: (count) => set({ resultCount: count }),
|
unfilteredResultCount: 0,
|
||||||
|
setResultCount: (count, unfilteredCount) =>
|
||||||
|
set({ resultCount: count, unfilteredResultCount: unfilteredCount ?? 0 }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type HotelData = {
|
|||||||
|
|
||||||
export type HotelCardListingProps = {
|
export type HotelCardListingProps = {
|
||||||
hotelData: HotelResponse[]
|
hotelData: HotelResponse[]
|
||||||
|
unfilteredHotelCount?: number
|
||||||
type?: HotelCardListingTypeEnum
|
type?: HotelCardListingTypeEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { SelectHotelBooking } from "./selectHotel"
|
|||||||
|
|
||||||
export interface HotelListingProps {
|
export interface HotelListingProps {
|
||||||
hotels: HotelResponse[]
|
hotels: HotelResponse[]
|
||||||
|
unfilteredHotelCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectHotelMapProps {
|
export interface SelectHotelMapProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user