Merge remote-tracking branch 'origin' into feature/tracking

This commit is contained in:
Linus Flood
2024-12-16 09:11:28 +01:00
42 changed files with 630 additions and 325 deletions

View File

@@ -218,7 +218,9 @@ export default function PaymentClient({
bedType: bedTypeMap[parseInt(child.bed.toString())],
})),
rateCode:
user || join || membershipNo ? room.counterRateCode : room.rateCode,
(user || join || membershipNo) && room.counterRateCode
? room.counterRateCode
: room.rateCode,
roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step.
guest: {
firstName,

View File

@@ -67,7 +67,7 @@ export default function SummaryUI({
}
: null
const showMemberPrice = !!(isMember || join || membershipNo)
const showMemberPrice = !!(isMember || join || membershipNo) && memberPrice
const diff = dt(booking.toDate).diff(booking.fromDate, "days")

View File

@@ -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<boolean>(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 (
<section className={styles.hotelCards}>
{hotels?.length ? (

View File

@@ -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]
)
}

View File

@@ -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<HotelData[]>([])
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
const [showSkeleton, setShowSkeleton] = useState<boolean>(false)
const [showSkeleton, setShowSkeleton] = useState<boolean>(true)
const listingContainerRef = useRef<HTMLDivElement | null>(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({
</Button>
<FilterAndSortModal filters={filterList} />
</div>
{showSkeleton ? (
<>
<div className={styles.skeletonContainer}>
<RoomCardSkeleton />
<RoomCardSkeleton />
</>
</div>
) : (
<HotelListing hotels={visibleHotels} />
)}

View File

@@ -48,4 +48,10 @@
padding: 0 0 var(--Spacing-x1);
position: static;
}
.skeletonContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
}

View File

@@ -47,9 +47,9 @@ export default async function SelectHotel({
const {
selectHotelParams,
searchParams,
adultsParams,
childrenParams,
child,
adultsInRoom,
childrenInRoom,
childrenInRoomArray,
} = reservationParams
const intl = await getIntl()
@@ -59,8 +59,8 @@ export default async function SelectHotel({
cityId: city.id,
roomStayStartDate: searchParams.fromDate,
roomStayEndDate: searchParams.toDate,
adults: adultsParams,
children: childrenParams?.toString(),
adults: adultsInRoom,
children: childrenInRoom,
})
)
@@ -115,10 +115,12 @@ export default async function SelectHotel({
searchTerm: searchParams.city,
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adultsParams,
noOfChildren: child?.length,
ageOfChildren: child?.map((c) => c.age).join(","),
childBedPreference: child?.map((c) => ChildBedMapEnum[c.bed]).join("|"),
noOfAdults: adultsInRoom,
noOfChildren: childrenInRoomArray?.length,
ageOfChildren: childrenInRoomArray?.map((c) => c.age).join(","),
childBedPreference: childrenInRoomArray
?.map((c) => ChildBedMapEnum[c.bed])
.join("|"),
noOfRooms: 1, // // TODO: Handle multiple rooms
duration: differenceInCalendarDays(departureDate, arrivalDate),
leadTime: differenceInCalendarDays(arrivalDate, new Date()),

View File

@@ -28,7 +28,10 @@ export default function PriceList({
const petRoomLocalPrice = petRoomPackage?.localPrice
const petRoomRequestedPrice = petRoomPackage?.requestedPrice
const showRequestedPrice = publicRequestedPrice && memberRequestedPrice
const showRequestedPrice =
publicRequestedPrice &&
memberRequestedPrice &&
publicRequestedPrice.currency !== publicLocalPrice.currency
const searchParams = useSearchParams()
const fromDate = searchParams.get("fromDate")
const toDate = searchParams.get("toDate")
@@ -114,27 +117,22 @@ export default function PriceList({
)}
</dd>
</div>
<div className={styles.priceRow}>
<dt>
<Caption
color={showRequestedPrice ? "uiTextMediumContrast" : "disabled"}
>
{intl.formatMessage({ id: "Approx." })}
</Caption>
</dt>
<dd>
{showRequestedPrice ? (
{showRequestedPrice && (
<div className={styles.priceRow}>
<dt>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}
</Caption>
</dt>
<dd>
<Caption color="uiTextMediumContrast">
{totalPublicRequestedPricePerNight}/
{totalMemberRequestedPricePerNight}{" "}
{publicRequestedPrice.currency}
</Caption>
) : (
<Caption color="disabled">- / - EUR</Caption>
)}
</dd>
</div>
</dd>
</div>
)}
</dl>
)
}

View File

@@ -7,6 +7,7 @@ import Label from "@/components/TempDesignSystem/Form/Label"
import Popover from "@/components/TempDesignSystem/Popover"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { rateCardEqualHeightSelector } from "../utils"
import PriceTable from "./PriceList"
import styles from "./flexibilityOption.module.css"
@@ -26,11 +27,13 @@ export default function FlexibilityOption({
if (!product) {
return (
<div className={styles.noPricesCard}>
<div className={`${styles.noPricesCard} ${rateCardEqualHeightSelector}`}>
<div className={styles.header}>
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
<Caption>{name}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
<div className={styles.priceType}>
<Caption>{name}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div>
</div>
<Label size="regular" className={styles.noPricesLabel}>
<Caption color="uiTextHighContrast" type="bold">
@@ -70,7 +73,7 @@ export default function FlexibilityOption({
value={publicPrice?.rateCode}
onClick={onClick}
/>
<div className={styles.card}>
<div className={`${styles.card} ${rateCardEqualHeightSelector}`}>
<div className={styles.header}>
<Popover
placement="bottom left"

View File

@@ -1,10 +1,15 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useMemo } from "react"
import { useCallback, useEffect, useMemo, useRef } from "react"
import { debounce } from "@/utils/debounce"
import RateSummary from "./RateSummary"
import RoomCard from "./RoomCard"
import { getHotelReservationQueryParams } from "./utils"
import {
getHotelReservationQueryParams,
rateCardEqualHeightSelector,
} from "./utils"
import styles from "./roomSelection.module.css"
@@ -23,9 +28,65 @@ export default function RoomSelection({
const router = useRouter()
const searchParams = useSearchParams()
const isUserLoggedIn = !!user
const roomRefs = useRef<HTMLLIElement[]>([])
const { roomConfigurations, rateDefinitions } = roomsAvailability
const equalizePriceOptionHeights = useCallback(() => {
if (!roomRefs.current.length) return
roomRefs.current.forEach((room) => {
const options = room.querySelectorAll<HTMLDivElement>(
`.${rateCardEqualHeightSelector}`
)
options.forEach((option) => {
option.style.height = "auto"
})
})
const numOptions =
roomRefs.current[0]?.querySelectorAll<HTMLDivElement>(
`.${rateCardEqualHeightSelector}`
).length || 0
for (let i = 0; i < numOptions; i++) {
let maxHeight = 0
roomRefs.current.forEach((room) => {
const option = room.querySelectorAll<HTMLDivElement>(
`.${rateCardEqualHeightSelector}`
)[i]
if (option) {
maxHeight = Math.max(maxHeight, option.offsetHeight)
}
})
roomRefs.current.forEach((room) => {
const option = room.querySelectorAll<HTMLDivElement>(
`.${rateCardEqualHeightSelector}`
)[i]
if (option) {
option.style.height = `${maxHeight}px`
}
})
}
}, [])
useEffect(() => {
const debouncedResizeHandler = debounce(function () {
equalizePriceOptionHeights()
})
const observer = new ResizeObserver(debouncedResizeHandler)
observer.observe(document.documentElement)
return () => {
if (observer) {
observer.unobserve(document.documentElement)
}
}
}, [roomRefs, equalizePriceOptionHeights])
const queryParams = useMemo(() => {
const params = new URLSearchParams(searchParams)
const searchParamsObject = getHotelReservationQueryParams(searchParams)
@@ -64,8 +125,13 @@ export default function RoomSelection({
onSubmit={handleSubmit}
>
<ul className={styles.roomList}>
{roomConfigurations.map((roomConfiguration) => (
<li key={roomConfiguration.roomTypeCode}>
{roomConfigurations.map((roomConfiguration, index) => (
<li
key={roomConfiguration.roomTypeCode}
ref={(el) => {
if (el) roomRefs.current[index] = el
}}
>
<RoomCard
hotelId={roomsAvailability.hotelId.toString()}
hotelType={hotelType}

View File

@@ -101,3 +101,5 @@ export function createQueryParamsForEnterDetails(
return searchParams
}
export const rateCardEqualHeightSelector = "rateCardEqualHeight"