diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index c7687362b..353bf2588 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,6 +1,6 @@ "use client" import { useSearchParams } from "next/navigation" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useMemo } from "react" import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" @@ -8,18 +8,18 @@ import { useHotelsMapStore } from "@/stores/hotels-map" import Alert from "@/components/TempDesignSystem/Alert" import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" +import { useScrollToTop } from "@/hooks/useScrollToTop" import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" +import { getSortedHotels } from "./utils" import styles from "./hotelCardListing.module.css" import { type HotelCardListingProps, HotelCardListingTypeEnum, - type HotelData, } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" import { AlertTypeEnum } from "@/types/enums/alert" export default function HotelCardListing({ @@ -29,82 +29,36 @@ export default function HotelCardListing({ const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) - const [showBackToTop, setShowBackToTop] = useState(false) const intl = useIntl() const { activeHotelCard } = useHotelsMapStore() + const { showBackToTop, scrollToTop } = useScrollToTop({ threshold: 490 }) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, [searchParams] ) - const sortedHotels = useMemo(() => { - switch (sortBy) { - case SortOrder.Name: - return [...hotelData].sort((a, b) => - a.hotelData.name.localeCompare(b.hotelData.name) - ) - case SortOrder.TripAdvisorRating: - return [...hotelData].sort( - (a, b) => - (b.hotelData.ratings?.tripAdvisor.rating ?? 0) - - (a.hotelData.ratings?.tripAdvisor.rating ?? 0) - ) - case SortOrder.Price: - const getPricePerNight = (hotel: HotelData): number => { - return ( - hotel.price?.member?.localPrice?.pricePerNight ?? - hotel.price?.public?.localPrice?.pricePerNight ?? - Infinity - ) - } - return [...hotelData].sort( - (a, b) => getPricePerNight(a) - getPricePerNight(b) - ) - case SortOrder.Distance: - default: - return [...hotelData].sort( - (a, b) => - a.hotelData.location.distanceToCentre - - b.hotelData.location.distanceToCentre - ) - } - }, [hotelData, sortBy]) + const sortedHotels = useMemo( + () => getSortedHotels({ hotels: hotelData, sortBy }), + [hotelData, sortBy] + ) const hotels = useMemo(() => { - if (activeFilters.length === 0) { - return sortedHotels - } + if (activeFilters.length === 0) return sortedHotels - const filteredHotels = sortedHotels.filter((hotel) => + return sortedHotels.filter((hotel) => activeFilters.every((appliedFilterId) => hotel.hotelData.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - - return filteredHotels }, [activeFilters, sortedHotels]) useEffect(() => { - const handleScroll = () => { - const hasScrolledPast = window.scrollY > 490 - setShowBackToTop(hasScrolledPast) - } - - window.addEventListener("scroll", handleScroll, { passive: true }) - return () => window.removeEventListener("scroll", handleScroll) - }, []) - - useEffect(() => { - setResultCount(hotels ? hotels.length : 0) + setResultCount(hotels?.length ?? 0) }, [hotels, setResultCount]) - function scrollToTop() { - window.scrollTo({ top: 0, behavior: "smooth" }) - } - return (
{hotels?.length ? ( diff --git a/components/HotelReservation/HotelCardListing/utils.ts b/components/HotelReservation/HotelCardListing/utils.ts new file mode 100644 index 000000000..1e57a6721 --- /dev/null +++ b/components/HotelReservation/HotelCardListing/utils.ts @@ -0,0 +1,35 @@ +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" + +export function getSortedHotels({ + hotels, + sortBy, +}: { + hotels: HotelData[] + sortBy: string +}) { + const getPricePerNight = (hotel: HotelData): number => + hotel.price?.member?.localPrice?.pricePerNight ?? + hotel.price?.public?.localPrice?.pricePerNight ?? + Infinity + + const sortingStrategies: Record< + string, + (a: HotelData, b: HotelData) => number + > = { + [SortOrder.Name]: (a: HotelData, b: HotelData) => + a.hotelData.name.localeCompare(b.hotelData.name), + [SortOrder.TripAdvisorRating]: (a: HotelData, b: HotelData) => + (b.hotelData.ratings?.tripAdvisor.rating ?? 0) - + (a.hotelData.ratings?.tripAdvisor.rating ?? 0), + [SortOrder.Price]: (a: HotelData, b: HotelData) => + getPricePerNight(a) - getPricePerNight(b), + [SortOrder.Distance]: (a: HotelData, b: HotelData) => + a.hotelData.location.distanceToCentre - + b.hotelData.location.distanceToCentre, + } + + return [...hotels].sort( + sortingStrategies[sortBy] ?? sortingStrategies[SortOrder.Distance] + ) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx index ed496c4cf..7dbb89171 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/index.tsx @@ -15,6 +15,7 @@ import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" +import { useScrollToTop } from "@/hooks/useScrollToTop" import { debounce } from "@/utils/debounce" import FilterAndSortModal from "../../FilterAndSortModal" @@ -41,13 +42,17 @@ export default function SelectHotelContent({ const isAboveMobile = useMediaQuery("(min-width: 768px)") const [visibleHotels, setVisibleHotels] = useState([]) - const [showBackToTop, setShowBackToTop] = useState(false) const [showSkeleton, setShowSkeleton] = useState(false) const listingContainerRef = useRef(null) const activeFilters = useHotelFilterStore((state) => state.activeFilters) const { activeHotelCard, activeHotelPin } = useHotelsMapStore() + const { showBackToTop, scrollToTop } = useScrollToTop({ + threshold: 490, + elementRef: listingContainerRef, + }) + const coordinates = useMemo( () => isAboveMobile @@ -66,28 +71,6 @@ export default function SelectHotelContent({ } }, [activeHotelCard, activeHotelPin]) - useEffect(() => { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - if (!hotelListingElement) return - - const handleScroll = () => { - const hasScrolledPast = hotelListingElement.scrollTop > 490 - setShowBackToTop(hasScrolledPast) - } - - hotelListingElement.addEventListener("scroll", handleScroll) - return () => hotelListingElement.removeEventListener("scroll", handleScroll) - }, []) - - function scrollToTop() { - const hotelListingElement = document.querySelector( - `.${styles.listingContainer}` - ) - hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) - } - const filteredHotelPins = useMemo( () => hotelPins.filter((hotel) => diff --git a/hooks/useScrollToTop.ts b/hooks/useScrollToTop.ts new file mode 100644 index 000000000..70f306783 --- /dev/null +++ b/hooks/useScrollToTop.ts @@ -0,0 +1,33 @@ +import { type RefObject,useEffect,useState } from "react" + + +interface UseScrollToTopProps { + threshold: number + elementRef?: RefObject +} + +export function useScrollToTop({ threshold, elementRef }: UseScrollToTopProps) { + const [showBackToTop, setShowBackToTop] = useState(false) + + useEffect(() => { + const element = elementRef?.current ?? window + + function handleScroll() { + const scrollTop = elementRef?.current + ? elementRef.current.scrollTop + : window.scrollY + setShowBackToTop(scrollTop > threshold) + } + + element.addEventListener("scroll", handleScroll, { passive: true }) + return () => element.removeEventListener("scroll", handleScroll) + }, [threshold, elementRef]) + + function scrollToTop() { + if (elementRef?.current) + elementRef.current.scrollTo({ top: 0, behavior: "smooth" }) + else window.scrollTo({ top: 0, behavior: "smooth" }) + } + + return { showBackToTop, scrollToTop } +}