From 5fb70866eac87b52fedb532ea16d8587d43da349 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 18 Nov 2024 14:02:32 +0000 Subject: [PATCH] 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 --- .../(standard)/select-hotel/page.module.css | 22 +++- .../(standard)/select-hotel/page.tsx | 8 +- .../HotelCardListing/index.tsx | 19 ++- .../filterAndSortModal.module.css | 99 ++++++++++++++++ .../SelectHotel/FilterAndSortModal/index.tsx | 87 ++++++++++++++ .../FilterCheckbox/filterCheckbox.module.css | 29 +++++ .../HotelFilter/FilterCheckbox/index.tsx | 35 ++++++ .../HotelFilter/hotelFilter.module.css | 7 -- .../SelectHotel/HotelFilter/index.tsx | 111 ++++++++++-------- .../HotelSorter/hotelSorter.module.css | 9 -- .../SelectHotel/HotelSorter/index.tsx | 25 ++-- .../MobileMapButtonContainer/index.tsx | 31 ++--- .../mobileMapButtonContainer.module.css | 4 +- i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 3 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + stores/hotel-filters.ts | 27 +++++ .../selectHotel/filterAndSortModal.ts | 5 + .../selectHotel/filterCheckbox.ts | 6 + .../selectHotel/hotelFilters.ts | 1 + .../selectHotel/hotelSorter.ts | 4 + 24 files changed, 434 insertions(+), 113 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx delete mode 100644 components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css create mode 100644 stores/hotel-filters.ts create mode 100644 types/components/hotelReservation/selectHotel/filterAndSortModal.ts create mode 100644 types/components/hotelReservation/selectHotel/filterCheckbox.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index 8bf36ee38..e42544196 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -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); diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 0493c9d70..791773f94 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -74,9 +74,11 @@ export default async function SelectHotelPage({ {city.name} {hotels.length} hotels - +
+ +
- +
@@ -118,7 +120,7 @@ export default async function SelectHotelPage({ />
)} - +
{!hotels.length && ( diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 4ba65ed9c..c4a5a1eee 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -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 (
diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css new file mode 100644 index 000000000..6768d2c56 --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -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); +} diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx new file mode 100644 index 000000000..be1a1bc9c --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -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 ( + <> + + + + + + {({ close }) => ( + <> +
+ +
+
+ +
+
+ +
+
+ + + +
+ + )} +
+
+
+
+ + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css new file mode 100644 index 000000000..4b3b94787 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css @@ -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; +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx new file mode 100644 index 000000000..6767f666b --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx @@ -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 ( + onChange(id)} + > + {({ isSelected }) => ( + <> + + + {isSelected && } + + {name} + + + )} + + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index c81b31cbd..8a4fcebff 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -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; - } -} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index a3b68b28e..c428894a3 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -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>({ - 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 ( -