Merged in feat/SW-342-filtering-and-sorting-mobile (pull request #919)
Feat/SW-342 filtering and sorting mobile * feat(SW-342): add sort and filter on mobile * Use zustand for state management * Add count and translations * Clear filters * Small fixes * Fixes Approved-by: Pontus Dreij
This commit is contained in:
@@ -20,10 +20,13 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.sorter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.link {
|
||||
@@ -47,6 +50,10 @@
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main {
|
||||
padding: var(--Spacing-x5);
|
||||
@@ -58,6 +65,11 @@
|
||||
var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.sorter {
|
||||
display: block;
|
||||
width: 339px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
@@ -65,6 +77,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sideBar {
|
||||
max-width: 340px;
|
||||
}
|
||||
.filter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
padding-bottom: var(--Spacing-x6);
|
||||
|
||||
@@ -74,9 +74,11 @@ export default async function SelectHotelPage({
|
||||
<Subtitle>{city.name}</Subtitle>
|
||||
<Preamble>{hotels.length} hotels</Preamble>
|
||||
</div>
|
||||
<HotelSorter />
|
||||
<div className={styles.sorter}>
|
||||
<HotelSorter discreet />
|
||||
</div>
|
||||
</div>
|
||||
<MobileMapButtonContainer city={searchParams.city} />
|
||||
<MobileMapButtonContainer filters={filterList} />
|
||||
</header>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.sideBar}>
|
||||
@@ -118,7 +120,7 @@ export default async function SelectHotelPage({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<HotelFilter filters={filterList} />
|
||||
<HotelFilter filters={filterList} className={styles.filter} />
|
||||
</div>
|
||||
<div className={styles.hotelList}>
|
||||
{!hotels.length && (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
import { DEFAULT_SORT } from "../SelectHotel/HotelSorter"
|
||||
@@ -22,6 +22,8 @@ export default function HotelCardListing({
|
||||
onHotelCardHover,
|
||||
}: HotelCardListingProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||
|
||||
const sortBy = useMemo(
|
||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||
@@ -57,17 +59,22 @@ export default function HotelCardListing({
|
||||
}, [hotelData, sortBy])
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
const appliedFilters = searchParams.get("filters")?.split(",")
|
||||
if (!appliedFilters || appliedFilters.length === 0) return sortedHotels
|
||||
if (activeFilters.length === 0) {
|
||||
setResultCount(sortedHotels.length)
|
||||
return sortedHotels
|
||||
}
|
||||
|
||||
return sortedHotels.filter((hotel) =>
|
||||
appliedFilters.every((appliedFilterId) =>
|
||||
const filteredHotels = sortedHotels.filter((hotel) =>
|
||||
activeFilters.every((appliedFilterId) =>
|
||||
hotel.hotelData.detailedFacilities.some(
|
||||
(facility) => facility.id.toString() === appliedFilterId
|
||||
)
|
||||
)
|
||||
)
|
||||
}, [searchParams, sortedHotels])
|
||||
|
||||
setResultCount(filteredHotels.length)
|
||||
return filteredHotels
|
||||
}, [activeFilters, sortedHotels, setResultCount])
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
@keyframes modal-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes modal-slide-up {
|
||||
from {
|
||||
bottom: -100%;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
height: var(--visual-viewport-height);
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-fade 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modal-fade 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: calc(100dvh - 20px);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
width: 100%;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-slide-up 200ms;
|
||||
}
|
||||
|
||||
&[data-existing] {
|
||||
animation: modal-slide-up 200ms reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sorter {
|
||||
padding: var(--Spacing-x2);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: var(--Spacing-x2);
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: right;
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
justify-self: flex-end;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2);
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Dialog as AriaDialog,
|
||||
DialogTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
|
||||
import { CloseLargeIcon, FilterIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import HotelFilter from "../HotelFilter"
|
||||
import HotelSorter from "../HotelSorter"
|
||||
|
||||
import styles from "./filterAndSortModal.module.css"
|
||||
|
||||
import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal"
|
||||
|
||||
export default function FilterAndSortModal({
|
||||
filters,
|
||||
}: FilterAndSortModalProps) {
|
||||
const intl = useIntl()
|
||||
const resultCount = useHotelFilterStore((state) => state.resultCount)
|
||||
const setFilters = useHotelFilterStore((state) => state.setFilters)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogTrigger>
|
||||
<Button intent="secondary" size="small" theme="base">
|
||||
<FilterIcon color="burgundy" />
|
||||
{intl.formatMessage({ id: "Filter and sort" })}
|
||||
</Button>
|
||||
<ModalOverlay className={styles.overlay} isDismissable>
|
||||
<Modal className={styles.modal}>
|
||||
<AriaDialog role="alertdialog" className={styles.content}>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<button
|
||||
onClick={close}
|
||||
type="button"
|
||||
className={styles.close}
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</header>
|
||||
<div className={styles.sorter}>
|
||||
<HotelSorter />
|
||||
</div>
|
||||
<div className={styles.filters}>
|
||||
<HotelFilter filters={filters} />
|
||||
</div>
|
||||
<footer className={styles.footer}>
|
||||
<Button
|
||||
intent="primary"
|
||||
size="medium"
|
||||
theme="base"
|
||||
onClick={close}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{ id: "See results" },
|
||||
{ count: resultCount }
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setFilters([])}
|
||||
intent="text"
|
||||
size="medium"
|
||||
theme="base"
|
||||
>
|
||||
{intl.formatMessage({ id: "Clear all filters" })}
|
||||
</Button>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</AriaDialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.container[data-selected] .checkbox {
|
||||
border: none;
|
||||
background: var(--UI-Input-Controls-Fill-Selected);
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
||||
border-radius: 4px;
|
||||
transition: all 200ms;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
forced-color-adjust: none;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { Checkbox as AriaCheckbox } from "react-aria-components"
|
||||
|
||||
import CheckIcon from "@/components/Icons/Check"
|
||||
|
||||
import styles from "./filterCheckbox.module.css"
|
||||
|
||||
import type { FilterCheckboxProps } from "@/types/components/hotelReservation/selectHotel/filterCheckbox"
|
||||
|
||||
export default function FilterCheckbox({
|
||||
isSelected,
|
||||
name,
|
||||
id,
|
||||
onChange,
|
||||
}: FilterCheckboxProps) {
|
||||
return (
|
||||
<AriaCheckbox
|
||||
className={styles.container}
|
||||
isSelected={isSelected}
|
||||
onChange={() => onChange(id)}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span className={styles.checkboxContainer}>
|
||||
<span className={styles.checkbox}>
|
||||
{isSelected && <CheckIcon color="white" />}
|
||||
</span>
|
||||
{name}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</AriaCheckbox>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
.container {
|
||||
min-width: 272px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container form {
|
||||
@@ -39,9 +38,3 @@
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import FilterCheckbox from "./FilterCheckbox"
|
||||
|
||||
import styles from "./hotelFilter.module.css"
|
||||
|
||||
import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
|
||||
export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||
export default function HotelFilter({ className, filters }: HotelFiltersProps) {
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const toggleFilter = useHotelFilterStore((state) => state.toggleFilter)
|
||||
const setFilters = useHotelFilterStore((state) => state.setFilters)
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
|
||||
const methods = useForm<Record<string, boolean | undefined>>({
|
||||
defaultValues: searchParams
|
||||
?.get("filters")
|
||||
?.split(",")
|
||||
.reduce((acc, curr) => ({ ...acc, [curr]: true }), {}),
|
||||
})
|
||||
const { watch, handleSubmit, getValues, register } = methods
|
||||
// Initialize the filters from the URL
|
||||
useEffect(() => {
|
||||
const filtersFromUrl = searchParams.get("filters")
|
||||
if (filtersFromUrl) {
|
||||
setFilters(filtersFromUrl.split(","))
|
||||
} else {
|
||||
setFilters([])
|
||||
}
|
||||
}, [searchParams, setFilters])
|
||||
|
||||
const submitFilter = useCallback(() => {
|
||||
// Update the URL when the filters changes
|
||||
useEffect(() => {
|
||||
const newSearchParams = new URLSearchParams(searchParams)
|
||||
const values = Object.entries(getValues())
|
||||
.filter(([_, value]) => !!value)
|
||||
.map(([key, _]) => key)
|
||||
.join(",")
|
||||
const values = activeFilters.join(",")
|
||||
|
||||
if (values === "") {
|
||||
newSearchParams.delete("filters")
|
||||
@@ -46,49 +51,51 @@ export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||
`${pathname}?${newSearchParams.toString()}`
|
||||
)
|
||||
}
|
||||
}, [getValues, pathname, searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => handleSubmit(submitFilter)())
|
||||
return () => subscription.unsubscribe()
|
||||
}, [handleSubmit, watch, submitFilter])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeFilters])
|
||||
|
||||
if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={styles.container}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
<Title as="h4">{intl.formatMessage({ id: "Filter by" })}</Title>
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage({ id: "Hotel facilities" })}
|
||||
</Subtitle>
|
||||
<ul>
|
||||
{filters.facilityFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<Checkbox name={filter.id.toString()}>{filter.name}</Checkbox>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<aside className={`${styles.container} ${className}`}>
|
||||
<Title as="h4">{intl.formatMessage({ id: "Filter by" })}</Title>
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>{intl.formatMessage({ id: "Hotel facilities" })}</Subtitle>
|
||||
<ul>
|
||||
{filters.facilityFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<FilterCheckbox
|
||||
name={filter.name}
|
||||
id={filter.id.toString()}
|
||||
onChange={() => toggleFilter(filter.id.toString())}
|
||||
isSelected={
|
||||
!!activeFilters.find((f) => f === filter.id.toString())
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>
|
||||
{intl.formatMessage({ id: "Hotel surroundings" })}
|
||||
</Subtitle>
|
||||
<ul>
|
||||
{filters.surroundingsFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<Checkbox name={filter.id.toString()}>{filter.name}</Checkbox>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
<div className={styles.facilities}>
|
||||
<Subtitle>{intl.formatMessage({ id: "Hotel surroundings" })}</Subtitle>
|
||||
<ul>
|
||||
{filters.surroundingsFilters.map((filter) => (
|
||||
<li key={`li-${filter.id}`} className={styles.filter}>
|
||||
<FilterCheckbox
|
||||
name={filter.name}
|
||||
id={filter.id.toString()}
|
||||
onChange={() => toggleFilter(filter.id.toString())}
|
||||
isSelected={
|
||||
!!activeFilters.find((f) => f === filter.id.toString())
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
width: 339px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,15 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import styles from "./hotelSorter.module.css"
|
||||
|
||||
import {
|
||||
type HotelSorterProps,
|
||||
type SortItem,
|
||||
SortOrder,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
|
||||
export const DEFAULT_SORT = SortOrder.Distance
|
||||
|
||||
export default function HotelSorter() {
|
||||
export default function HotelSorter({ discreet }: HotelSorterProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const intl = useIntl()
|
||||
@@ -52,16 +51,14 @@ export default function HotelSorter() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Select
|
||||
items={sortItems}
|
||||
defaultSelectedKey={searchParams.get("sort") ?? DEFAULT_SORT}
|
||||
label={intl.formatMessage({ id: "Sort by" })}
|
||||
name="sort"
|
||||
showRadioButton
|
||||
discreet
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
items={sortItems}
|
||||
defaultSelectedKey={searchParams.get("sort") ?? DEFAULT_SORT}
|
||||
label={intl.formatMessage({ id: "Sort by" })}
|
||||
name="sort"
|
||||
showRadioButton
|
||||
discreet={discreet}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,26 +4,28 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { selectHotelMap } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { FilterIcon, MapIcon } from "@/components/Icons"
|
||||
import { MapIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import FilterAndSortModal from "../FilterAndSortModal"
|
||||
|
||||
import styles from "./mobileMapButtonContainer.module.css"
|
||||
|
||||
export default function MobileMapButtonContainer({ city }: { city: string }) {
|
||||
import { CategorizedFilters } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
|
||||
|
||||
export default function MobileMapButtonContainer({
|
||||
filters,
|
||||
}: {
|
||||
filters: CategorizedFilters
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
return (
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
asChild
|
||||
variant="icon"
|
||||
intent="secondary"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<Button asChild variant="icon" intent="secondary" size="small">
|
||||
<Link
|
||||
href={`${selectHotelMap[lang]}`}
|
||||
keepSearchParams
|
||||
@@ -33,16 +35,7 @@ export default function MobileMapButtonContainer({ city }: { city: string }) {
|
||||
{intl.formatMessage({ id: "See on map" })}
|
||||
</Link>
|
||||
</Button>
|
||||
{/* TODO: Add filter toggle */}
|
||||
<Button
|
||||
variant="icon"
|
||||
intent="secondary"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<FilterIcon color="burgundy" />
|
||||
{intl.formatMessage({ id: "Filter and sort" })}
|
||||
</Button>
|
||||
<FilterAndSortModal filters={filters} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: 1;
|
||||
.buttonContainer > * {
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Cities": "Byer",
|
||||
"City": "By",
|
||||
"City/State": "By/Stat",
|
||||
"Clear all filters": "Ryd alle filtre",
|
||||
"Clear searches": "Ryd søgninger",
|
||||
"Click here to log in": "Klik her for at logge ind",
|
||||
"Close": "Tæt",
|
||||
@@ -122,6 +123,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Fair": "Messe",
|
||||
"Filter": "Filter",
|
||||
"Filter and sort": "Filtrer og sorter",
|
||||
"Filter by": "Filtrer efter",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotel",
|
||||
@@ -309,6 +311,7 @@
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
"See map": "Vis kort",
|
||||
"See on map": "Se på kort",
|
||||
"See results": "Se resultater ({ count })",
|
||||
"See room details": "Se værelsesdetaljer",
|
||||
"See rooms": "Se værelser",
|
||||
"Select a country": "Vælg et land",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Cities": "Städte",
|
||||
"City": "Stadt",
|
||||
"City/State": "Stadt/Zustand",
|
||||
"Clear all filters": "Alle Filter löschen",
|
||||
"Clear searches": "Suche löschen",
|
||||
"Click here to log in": "Klicken Sie hier, um sich einzuloggen",
|
||||
"Close": "Schließen",
|
||||
@@ -122,6 +123,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Fair": "Messe",
|
||||
"Filter": "Filter",
|
||||
"Filter and sort": "Filtern und sortieren",
|
||||
"Filter by": "Filtern nach",
|
||||
"Find booking": "Buchung finden",
|
||||
"Find hotels": "Hotels finden",
|
||||
@@ -308,6 +310,7 @@
|
||||
"See less FAQ": "Weniger anzeigen FAQ",
|
||||
"See map": "Karte anzeigen",
|
||||
"See on map": "Karte ansehen",
|
||||
"See results": "Ergebnisse anzeigen ({ count })",
|
||||
"See room details": "Zimmerdetails ansehen",
|
||||
"See rooms": "Zimmer ansehen",
|
||||
"Select a country": "Wähle ein Land",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"Cities": "Cities",
|
||||
"City": "City",
|
||||
"City/State": "City/State",
|
||||
"Clear all filters": "Clear all filters",
|
||||
"Clear searches": "Clear searches",
|
||||
"Click here to log in": "Click here to log in",
|
||||
"Close": "Close",
|
||||
@@ -131,6 +132,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Fair": "Fair",
|
||||
"Filter": "Filter",
|
||||
"Filter and sort": "Filter and sort",
|
||||
"Filter by": "Filter by",
|
||||
"Find booking": "Find booking",
|
||||
"Find hotels": "Find hotels",
|
||||
@@ -337,6 +339,7 @@
|
||||
"See less FAQ": "See less FAQ",
|
||||
"See map": "See map",
|
||||
"See on map": "See on map",
|
||||
"See results": "See results ({ count })",
|
||||
"See room details": "See room details",
|
||||
"See rooms": "See rooms",
|
||||
"See you soon!": "See you soon!",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Cities": "Kaupungit",
|
||||
"City": "Kaupunki",
|
||||
"City/State": "Kaupunki/Osavaltio",
|
||||
"Clear all filters": "Tyhjennä kaikki suodattimet",
|
||||
"Clear searches": "Tyhjennä haut",
|
||||
"Click here to log in": "Napsauta tästä kirjautuaksesi sisään",
|
||||
"Close": "Kiinni",
|
||||
@@ -122,6 +123,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Fair": "Messukeskus",
|
||||
"Filter": "Suodatin",
|
||||
"Filter and sort": "Suodata ja lajittele",
|
||||
"Filter by": "Suodatusperuste",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Find hotels": "Etsi hotelleja",
|
||||
@@ -310,6 +312,7 @@
|
||||
"See less FAQ": "Katso vähemmän UKK",
|
||||
"See map": "Näytä kartta",
|
||||
"See on map": "Näytä kartalla",
|
||||
"See results": "Katso tulokset ({ count })",
|
||||
"See room details": "Katso huoneen tiedot",
|
||||
"See rooms": "Katso huoneet",
|
||||
"Select a country": "Valitse maa",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Cities": "Byer",
|
||||
"City": "By",
|
||||
"City/State": "By/Stat",
|
||||
"Clear all filters": "Fjern alle filtre",
|
||||
"Clear searches": "Tømme søk",
|
||||
"Click here to log in": "Klikk her for å logge inn",
|
||||
"Close": "Lukk",
|
||||
@@ -121,6 +122,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Fair": "Messe",
|
||||
"Filter": "Filter",
|
||||
"Filter and sort": "Filtrer og sorter",
|
||||
"Filter by": "Filtrer etter",
|
||||
"Find booking": "Finn booking",
|
||||
"Find hotels": "Finn hotell",
|
||||
@@ -307,6 +309,7 @@
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
"See map": "Vis kart",
|
||||
"See on map": "Se på kart",
|
||||
"See results": "Se resultater ({ count })",
|
||||
"See room details": "Se detaljer om rommet",
|
||||
"See rooms": "Se rom",
|
||||
"Select a country": "Velg et land",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Cities": "Städer",
|
||||
"City": "Ort",
|
||||
"City/State": "Ort",
|
||||
"Clear all filters": "Rensa alla filter",
|
||||
"Clear searches": "Rensa tidigare sökningar",
|
||||
"Click here to log in": "Klicka här för att logga in",
|
||||
"Close": "Stäng",
|
||||
@@ -121,6 +122,7 @@
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Fair": "Mässa",
|
||||
"Filter": "Filter",
|
||||
"Filter and sort": "Filtrera och sortera",
|
||||
"Filter by": "Filtrera på",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Find hotels": "Hitta hotell",
|
||||
@@ -307,6 +309,7 @@
|
||||
"See less FAQ": "See färre FAQ",
|
||||
"See map": "Visa karta",
|
||||
"See on map": "Se på karta",
|
||||
"See results": "Se resultat ({ count })",
|
||||
"See room details": "Se rumsdetaljer",
|
||||
"See rooms": "Se rum",
|
||||
"Select a country": "Välj ett land",
|
||||
|
||||
27
stores/hotel-filters.ts
Normal file
27
stores/hotel-filters.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { create } from "zustand"
|
||||
|
||||
interface HotelFilterState {
|
||||
activeFilters: string[]
|
||||
toggleFilter: (filterId: string) => void
|
||||
setFilters: (filters: string[]) => void
|
||||
resultCount: number
|
||||
setResultCount: (count: number) => void
|
||||
}
|
||||
|
||||
export const useHotelFilterStore = create<HotelFilterState>((set) => ({
|
||||
activeFilters: [],
|
||||
|
||||
setFilters: (filters) => set({ activeFilters: filters }),
|
||||
|
||||
toggleFilter: (filterId: string) =>
|
||||
set((state) => {
|
||||
const isActive = state.activeFilters.includes(filterId)
|
||||
const newFilters = isActive
|
||||
? state.activeFilters.filter((id) => id !== filterId)
|
||||
: [...state.activeFilters, filterId]
|
||||
return { activeFilters: newFilters }
|
||||
}),
|
||||
|
||||
resultCount: 0,
|
||||
setResultCount: (count) => set({ resultCount: count }),
|
||||
}))
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CategorizedFilters } from "./hotelFilters"
|
||||
|
||||
export type FilterAndSortModalProps = {
|
||||
filters: CategorizedFilters
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type FilterCheckboxProps = {
|
||||
name: string
|
||||
id: string
|
||||
isSelected: boolean
|
||||
onChange: (filterId: string) => void
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export type CategorizedFilters = {
|
||||
}
|
||||
export type HotelFiltersProps = {
|
||||
filters: CategorizedFilters
|
||||
className?: string
|
||||
}
|
||||
|
||||
export type Filter = {
|
||||
|
||||
@@ -9,3 +9,7 @@ export type SortItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type HotelSorterProps = {
|
||||
discreet?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user