diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 29caabded..34ae44b6f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1,17 +1,13 @@ import { notFound } from "next/navigation" import { Suspense } from "react" -import { getLocations } from "@/lib/trpc/memoizedRequests" - import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer" import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { MapContainer } from "@/components/MapContainer" import { setLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "../../utils" + import styles from "./page.module.css" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" @@ -22,25 +18,12 @@ export default async function SelectHotelMapPage({ searchParams, }: PageArgs) { setLang(params.lang) - const locations = await getLocations() + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { city, adultsInRoom, childrenInRoom } = searchDetails - if (!locations || "error" in locations) { - return null - } - const city = locations.data.find( - (location) => - location.name.toLowerCase() === searchParams.city.toLowerCase() - ) if (!city) return notFound() - const selectHotelParams = new URLSearchParams(searchParams) - const selectHotelParamsObject = - getHotelReservationQueryParams(selectHotelParams) - const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const childrenInRoom = selectHotelParamsObject.room[0].child - ? generateChildrenString(selectHotelParamsObject.room[0].child) - : undefined // TODO: Handle multiple rooms - return (
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 3c3435dd4..2d4aea4af 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,16 +1,12 @@ import { notFound } from "next/navigation" import { Suspense } from "react" -import { getLocations } from "@/lib/trpc/memoizedRequests" - import SelectHotel from "@/components/HotelReservation/SelectHotel" import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton" -import { - generateChildrenString, - getHotelReservationQueryParams, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" +import { getHotelSearchDetails } from "../utils" + import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -19,44 +15,22 @@ export default async function SelectHotelPage({ searchParams, }: PageArgs) { setLang(params.lang) - const locations = await getLocations() - - if (!locations || "error" in locations) { - return null - } - const city = locations.data.find( - (location) => - location.name.toLowerCase() === searchParams.city.toLowerCase() - ) + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { city, urlSearchParams, adultsInRoom, childrenInRoom } = searchDetails if (!city) return notFound() - const selectHotelParams = new URLSearchParams(searchParams) - const selectHotelParamsObject = - getHotelReservationQueryParams(selectHotelParams) - - if ( - !selectHotelParamsObject.room || - selectHotelParamsObject.room.length === 0 - ) { - return notFound() - } - - const adultsParams = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const childrenParams = selectHotelParamsObject.room[0].child - ? generateChildrenString(selectHotelParamsObject.room[0].child) - : undefined // TODO: Handle multiple rooms - const reservationParams = { - selectHotelParams, + selectHotelParams: urlSearchParams, searchParams, - adultsParams, - childrenParams, + adultsInRoom, + childrenInRoom, } return ( } > ) { setLang(params.lang) + const searchDetails = await getHotelSearchDetails({ searchParams }) + if (!searchDetails) return notFound() + const { hotel, adultsInRoom, childrenInRoomArray } = searchDetails - const locations = await getLocations() - if (!locations || "error" in locations) { - return null - } - const hotel = locations.data.find( - (location) => - "operaId" in location && location.operaId == searchParams.hotel - ) - if (!hotel) { - return notFound() - } - const selectRoomParams = new URLSearchParams(searchParams) - const selectRoomParamsObject = - getHotelReservationQueryParams(selectRoomParams) - - if (!selectRoomParamsObject.room) { - return notFound() - } + if (!hotel) return notFound() const { fromDate, toDate } = getValidDates( searchParams.fromDate, searchParams.toDate ) - const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms - const children = selectRoomParamsObject.room[0].child // TODO: Handle multiple rooms - - const [hotelData, hotelDataError] = await safeTry( - getHotelData({ hotelId: searchParams.hotel, language: params.lang }) - ) - - if (!hotelData && !hotelDataError) { - return notFound() - } - - const hotelId = +searchParams.hotel + const hotelId = +hotel.id return ( <> }> @@ -74,8 +46,8 @@ export default async function SelectRatePage({ lang={params.lang} fromDate={fromDate.toDate()} toDate={toDate.toDate()} - adultCount={adults} - childArray={children} + adultCount={adultsInRoom} + childArray={childrenInRoomArray} /> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts new file mode 100644 index 000000000..2a0ae8d42 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/utils.ts @@ -0,0 +1,77 @@ +import { notFound } from "next/navigation" + +import { getLocations } from "@/lib/trpc/memoizedRequests" + +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" + +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { + Child, + SelectRateSearchParams, +} from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Location } from "@/types/trpc/routers/hotel/locations" + +interface HotelSearchDetails { + city: Location | null + hotel: Location | null + urlSearchParams?: URLSearchParams + adultsInRoom: number + childrenInRoom?: string + childrenInRoomArray?: Child[] +} + +export async function getHotelSearchDetails({ + searchParams, +}: { + searchParams: + | (SelectHotelSearchParams & { + [key: string]: string + }) + | (SelectRateSearchParams & { + [key: string]: string + }) +}): Promise { + const locations = await getLocations() + + if (!locations || "error" in locations) return null + + const city = locations.data.find( + (location) => + location.name.toLowerCase() === searchParams.city?.toLowerCase() + ) + const hotel = locations.data.find( + (location) => + "operaId" in location && location.operaId == searchParams.hotel + ) + + if (!city && !hotel) return notFound() + + const urlSearchParams = new URLSearchParams(searchParams) + const searchParamsObject = getHotelReservationQueryParams(urlSearchParams) + + let adultsInRoom = 1 + let childrenInRoom: string | undefined = undefined + let childrenInRoomArray: Child[] | undefined = undefined + + if (searchParamsObject.room && searchParamsObject.room.length > 0) { + adultsInRoom = searchParamsObject.room[0].adults // TODO: Handle multiple rooms + childrenInRoom = searchParamsObject.room[0].child + ? generateChildrenString(searchParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + childrenInRoomArray = searchParamsObject.room[0].child + ? searchParamsObject.room[0].child + : undefined // TODO: Handle multiple rooms + } + + return { + city: city ?? null, + hotel: hotel ?? null, + urlSearchParams, + adultsInRoom, + childrenInRoom, + childrenInRoomArray, + } +} 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..a9b72d220 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 [showSkeleton, setShowSkeleton] = useState(true) 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) => @@ -102,7 +85,7 @@ export default function SelectHotelContent({ const visibleHotels = getVisibleHotels(hotels, filteredHotelPins, map) setVisibleHotels(visibleHotels) setTimeout(() => { - setShowSkeleton(true) + setShowSkeleton(false) }, SKELETON_LOAD_DELAY) }, [hotels, filteredHotelPins, map]) @@ -116,7 +99,7 @@ export default function SelectHotelContent({ () => debounce(() => { if (!map) return - setShowSkeleton(false) + setShowSkeleton(true) getHotelCards() }, 100), [map, getHotelCards] @@ -155,11 +138,12 @@ export default function SelectHotelContent({
+ {showSkeleton ? ( - <> +
- +
) : ( )} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css index 811f94d68..ae6b81191 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContent/selectHotelMapContent.module.css @@ -48,4 +48,10 @@ padding: 0 0 var(--Spacing-x1); position: static; } + + .skeletonContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + } } diff --git a/components/HotelReservation/SelectHotel/index.tsx b/components/HotelReservation/SelectHotel/index.tsx index 2795d0c00..16c17520e 100644 --- a/components/HotelReservation/SelectHotel/index.tsx +++ b/components/HotelReservation/SelectHotel/index.tsx @@ -34,7 +34,7 @@ export default async function SelectHotel({ params, reservationParams, }: SelectHotelProps) { - const { selectHotelParams, searchParams, adultsParams, childrenParams } = + const { selectHotelParams, searchParams, adultsInRoom, childrenInRoom } = reservationParams const intl = await getIntl() @@ -44,8 +44,8 @@ export default async function SelectHotel({ cityId: city.id, roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, - adults: adultsParams, - children: childrenParams?.toString(), + adults: adultsInRoom, + children: childrenInRoom, }) ) diff --git a/hooks/useScrollToTop.ts b/hooks/useScrollToTop.ts new file mode 100644 index 000000000..39ff5da0c --- /dev/null +++ b/hooks/useScrollToTop.ts @@ -0,0 +1,32 @@ +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 } +} diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index 016645813..4303d65a7 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -1,7 +1,6 @@ -import { Lang } from "@/constants/languages" - import type { CheckInData, Hotel, ParkingData } from "@/types/hotel" import type { Location } from "@/types/trpc/routers/hotel/locations" +import type { Lang } from "@/constants/languages" import type { SelectHotelSearchParams } from "./selectHotelSearchParams" export enum AvailabilityEnum { @@ -46,9 +45,9 @@ export interface SelectHotelProps { lang: Lang } reservationParams: { - selectHotelParams: URLSearchParams + selectHotelParams: URLSearchParams | undefined searchParams: SelectHotelSearchParams - adultsParams: number - childrenParams: string | undefined + adultsInRoom: number + childrenInRoom: string | undefined } } diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 15f50da7c..164222a88 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -1,6 +1,5 @@ -import { PaymentMethodEnum } from "@/constants/booking" - -import { CreditCard, SafeUser } from "@/types/user" +import type { CreditCard, SafeUser } from "@/types/user" +import type { PaymentMethodEnum } from "@/constants/booking" export interface SectionProps { nextPath: string