From b6d8431e82b7e681eef5770ffdb35763f1a52f79 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Fri, 31 Jan 2025 11:09:46 +0000 Subject: [PATCH] Merged in feat/SW-964-Sticky-summary-multiroom (pull request #1231) Feat/SW-964 Sticky summary multiroom (UX) * feat(SW-964) Multiroom support for summary in select-rate * feat(SW-964) added utils for calculateTotalPrice * feat(SW-964) Removed duplicated code Approved-by: Tobias Johansson --- .../SelectRate/RateSummary/index.tsx | 120 +++++++++++------- .../RateSummary/rateSummary.module.css | 5 +- .../SelectRate/RateSummary/utils.ts | 58 +++++++++ .../SelectRate/Rooms/index.tsx | 32 +---- .../selectRate/rateSummary.ts | 3 +- 5 files changed, 143 insertions(+), 75 deletions(-) create mode 100644 components/HotelReservation/SelectRate/RateSummary/utils.ts diff --git a/components/HotelReservation/SelectRate/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RateSummary/index.tsx index bac210d79..cbded87de 100644 --- a/components/HotelReservation/SelectRate/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RateSummary/index.tsx @@ -1,3 +1,5 @@ +"use client" + import { useEffect, useState } from "react" import { useIntl } from "react-intl" @@ -13,6 +15,8 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { formatPrice } from "@/utils/numberFormatting" +import { calculateTotalPrice } from "./utils" + import styles from "./rateSummary.module.css" import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary" @@ -22,6 +26,7 @@ export default function RateSummary({ isUserLoggedIn, packages, roomsAvailability, + rooms, }: RateSummaryProps) { const intl = useIntl() const [isVisible, setIsVisible] = useState(false) @@ -34,87 +39,107 @@ export default function RateSummary({ }, []) const selectedRateSummary = getSelectedRateSummary() - + const totalRoomsRequired = rooms?.length || 1 if (selectedRateSummary.length === 0) return null - const { - member, - public: publicRate, - features, - roomType, - priceName, - priceTerm, - } = selectedRateSummary[0] // TODO: Support multiple rooms - - const isPetRoomSelected = features.some( - (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM - ) - const petRoomPackage = packages?.find( (pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM ) - const petRoomLocalPrice = - isPetRoomSelected && petRoomPackage?.localPrice.totalPrice - ? Number(petRoomPackage?.localPrice.totalPrice) - : 0 - const petRoomRequestedPrice = - isPetRoomSelected && petRoomPackage?.requestedPrice.totalPrice - ? Number(petRoomPackage?.requestedPrice.totalPrice) - : 0 - - const priceToShow = isUserLoggedIn && member ? member : publicRate - - const totalPriceToShow = { - localPrice: { - currency: priceToShow.localPrice.currency, - price: priceToShow.localPrice.pricePerStay + petRoomLocalPrice, - }, - requestedPrice: !priceToShow.requestedPrice - ? undefined - : { - currency: priceToShow.requestedPrice.currency, - price: - priceToShow.requestedPrice.pricePerStay + petRoomRequestedPrice, - }, - } + const totalPriceToShow = calculateTotalPrice( + selectedRateSummary, + isUserLoggedIn, + petRoomPackage + ) + const isAllRoomsSelected = selectedRateSummary.length === totalRoomsRequired const checkInDate = new Date(roomsAvailability.checkInDate) const checkOutDate = new Date(roomsAvailability.checkOutDate) const nights = dt(checkOutDate).diff(dt(checkInDate), "days") - const showMemberDiscountBanner = member && !isUserLoggedIn + const hasMemberRates = selectedRateSummary.some((room) => room.member) + + const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn const summaryPriceText = `${intl.formatMessage( { id: "{totalNights, plural, one {# night} other {# nights}}" }, { totalNights: nights } )}, ${intl.formatMessage( { id: "{totalAdults, plural, one {# adult} other {# adults}}" }, - { totalAdults: roomsAvailability.occupancy?.adults } + { totalAdults: rooms.reduce((acc, room) => acc + room.adults, 0) } )}${ - roomsAvailability.occupancy?.children?.length + rooms.some((room) => room.childrenInRoom?.length) ? `, ${intl.formatMessage( { id: "{totalChildren, plural, one {# child} other {# children}}" }, - { totalChildren: roomsAvailability.occupancy.children.length } + { + totalChildren: rooms.reduce( + (acc, room) => acc + (room.childrenInRoom?.length ?? 0), + 0 + ), + } )}` : "" - }` + }, ${intl.formatMessage( + { id: "{totalRooms, plural, one {# room} other {# rooms}}" }, + { + totalRooms: rooms.length, + } + )}` return (
{showMemberDiscountBanner && }
- {roomType} - {`${priceName}, ${priceTerm}`} + {selectedRateSummary.map((room, index) => ( +
+ + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: index + 1 } + )} + + {room.roomType} + {`${room.priceName}, ${room.priceTerm}`} +
+ ))} + {/* Render unselected rooms */} + {Array.from({ + length: totalRoomsRequired - selectedRateSummary.length, + }).map((_, index) => ( +
+ + {intl.formatMessage( + { id: "Room {roomIndex}" }, + { roomIndex: selectedRateSummary.length + index + 1 } + )} + + + {intl.formatMessage({ id: "Select room" })} + +
+ ))}
{showMemberDiscountBanner && (
{ + const memberPrice = + room.member?.localPrice.pricePerStay ?? 0 + const isPetRoom = room.features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM + ) + const petRoomPrice = + isPetRoom && petRoomPackage + ? Number(petRoomPackage.localPrice.totalPrice || 0) + : 0 + return total + memberPrice + petRoomPrice + }, 0), + currency: + selectedRateSummary[0].member?.localPrice.currency ?? + selectedRateSummary[0].public.localPrice.currency, }} />
@@ -177,6 +202,7 @@ export default function RateSummary({ type="submit" theme="base" className={styles.continueButton} + disabled={!isAllRoomsSelected} > {intl.formatMessage({ id: "Continue" })} diff --git a/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css b/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css index 9f910233b..fac373840 100644 --- a/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css +++ b/components/HotelReservation/SelectRate/RateSummary/rateSummary.module.css @@ -79,10 +79,13 @@ } .petInfo, .promoContainer, - .summaryText, .summaryPriceTextDesktop { display: block; } + .summaryText { + display: flex; + gap: var(--Spacing-x2); + } .summaryPriceTextMobile { display: none; } diff --git a/components/HotelReservation/SelectRate/RateSummary/utils.ts b/components/HotelReservation/SelectRate/RateSummary/utils.ts new file mode 100644 index 000000000..93319dfa6 --- /dev/null +++ b/components/HotelReservation/SelectRate/RateSummary/utils.ts @@ -0,0 +1,58 @@ +import { + type RoomPackage, + RoomPackageCodeEnum, +} from "@/types/components/hotelReservation/selectRate/roomFilter" +import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate" + +interface TotalPrice { + localPrice: { currency: string; price: number } + requestedPrice?: { currency: string; price: number } +} + +export const calculateTotalPrice = ( + selectedRateSummary: Rate[], + isUserLoggedIn: boolean, + petRoomPackage: RoomPackage | undefined +) => { + return selectedRateSummary.reduce( + (total, room) => { + const priceToUse = + isUserLoggedIn && room.member ? room.member : room.public + const isPetRoom = room.features.some( + (feature) => feature.code === RoomPackageCodeEnum.PET_ROOM + ) + const petRoomPrice = + isPetRoom && petRoomPackage + ? isUserLoggedIn + ? Number(petRoomPackage.localPrice.totalPrice || 0) + : Number(petRoomPackage.requestedPrice.totalPrice || 0) + : 0 + + return { + localPrice: { + currency: priceToUse.localPrice.currency, + price: + total.localPrice.price + + priceToUse.localPrice.pricePerStay + + petRoomPrice, + }, + requestedPrice: priceToUse.requestedPrice + ? { + currency: priceToUse.requestedPrice.currency, + price: + (total.requestedPrice?.price ?? 0) + + priceToUse.requestedPrice.pricePerStay + + petRoomPrice, + } + : undefined, + } + }, + { + localPrice: { + currency: selectedRateSummary[0].public.localPrice.currency, + price: 0, + }, + requestedPrice: undefined, + } + ) +} diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index aa3d46b06..a09f99946 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -14,7 +14,6 @@ import { convertObjToSearchParams, convertSearchParamsToObj } from "@/utils/url" import RateSummary from "../RateSummary" import { RoomSelectionPanel } from "../RoomSelectionPanel" import SelectedRoomPanel from "../SelectedRoomPanel" -import { filterDuplicateRoomTypesByLowestPrice } from "./utils" import { roomSelectionPanelVariants } from "./variants" import styles from "./rooms.module.css" @@ -25,7 +24,6 @@ import { } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" export default function Rooms({ availablePackages, @@ -37,6 +35,7 @@ export default function Rooms({ const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() + const intl = useIntl() const hotelId = searchParams.get("hotel") const arrivalDate = searchParams.get("fromDate") @@ -47,6 +46,7 @@ export default function Rooms({ const { selectedPackagesByRoom, + visibleRooms, setVisibleRooms, setRoomsAvailability, getFilteredRooms, @@ -62,33 +62,10 @@ export default function Rooms({ const isMultipleRooms = bookingWidgetSearchData.rooms.length > 1 - const intl = useIntl() - useEffect(() => { initializeRates(bookingWidgetSearchData.rooms.length) }, [initializeRates, bookingWidgetSearchData.rooms.length]) - const visibleRooms: RoomConfiguration[] = useMemo(() => { - const deduped = filterDuplicateRoomTypesByLowestPrice( - roomsAvailability.roomConfigurations - ) - - const separated = deduped.reduce<{ - available: RoomConfiguration[] - notAvailable: RoomConfiguration[] - }>( - (acc, curr) => { - if (curr.status === "NotAvailable") { - return { ...acc, notAvailable: [...acc.notAvailable, curr] } - } - return { ...acc, available: [...acc.available, curr] } - }, - { available: [], notAvailable: [] } - ) - - return [...separated.available, ...separated.notAvailable] - }, [roomsAvailability.roomConfigurations]) - const defaultPackages: DefaultFilterOptions[] = useMemo( () => [ { @@ -197,7 +174,9 @@ export default function Rooms({ const SCROLL_OFFSET = 100 const roomElements = document.querySelectorAll(`.${styles.roomContainer}`) const index = selectedRates.findIndex((rate) => rate === undefined) - const selectedRoom = roomElements[index - 1] + + const targetIndex = index === -1 ? selectedRates.length - 1 : index - 1 + const selectedRoom = roomElements[targetIndex] if (selectedRoom) { const elementPosition = selectedRoom.getBoundingClientRect().top @@ -286,6 +265,7 @@ export default function Rooms({ isUserLoggedIn={isUserLoggedIn} packages={availablePackages} roomsAvailability={roomsAvailability} + rooms={bookingWidgetSearchData.rooms} /> )} diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 470f2bf3d..0a7b55169 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,9 +1,10 @@ import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { RoomPackages } from "./roomFilter" -import type { Rate } from "./selectRate" +import type { Room } from "./selectRate" export interface RateSummaryProps { isUserLoggedIn: boolean packages: RoomPackages | undefined roomsAvailability: RoomsAvailability + rooms: Room[] }