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
This commit is contained in:
Pontus Dreij
2025-01-31 11:09:46 +00:00
parent f82de5aad7
commit b6d8431e82
5 changed files with 143 additions and 75 deletions

View File

@@ -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 (
<div className={styles.summary} data-visible={isVisible}>
{showMemberDiscountBanner && <SignupPromoMobile />}
<div className={styles.content}>
<div className={styles.summaryText}>
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
<Body color="uiTextMediumContrast">{`${priceName}, ${priceTerm}`}</Body>
{selectedRateSummary.map((room, index) => (
<div key={index} className={styles.roomSummary}>
<Subtitle color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: index + 1 }
)}
</Subtitle>
<Body color="uiTextMediumContrast">{room.roomType}</Body>
<Caption color="uiTextMediumContrast">{`${room.priceName}, ${room.priceTerm}`}</Caption>
</div>
))}
{/* Render unselected rooms */}
{Array.from({
length: totalRoomsRequired - selectedRateSummary.length,
}).map((_, index) => (
<div key={`unselected-${index}`} className={styles.roomSummary}>
<Subtitle color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "Room {roomIndex}" },
{ roomIndex: selectedRateSummary.length + index + 1 }
)}
</Subtitle>
<Body color="uiTextPlaceholder">
{intl.formatMessage({ id: "Select room" })}
</Body>
</div>
))}
</div>
<div className={styles.summaryPriceContainer}>
{showMemberDiscountBanner && (
<div className={styles.promoContainer}>
<SignupPromoDesktop
memberPrice={{
amount: member.localPrice.pricePerStay + petRoomLocalPrice,
currency: member.localPrice.currency,
amount: selectedRateSummary.reduce((total, room) => {
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,
}}
/>
</div>
@@ -177,6 +202,7 @@ export default function RateSummary({
type="submit"
theme="base"
className={styles.continueButton}
disabled={!isAllRoomsSelected}
>
{intl.formatMessage({ id: "Continue" })}
</Button>

View File

@@ -79,10 +79,13 @@
}
.petInfo,
.promoContainer,
.summaryText,
.summaryPriceTextDesktop {
display: block;
}
.summaryText {
display: flex;
gap: var(--Spacing-x2);
}
.summaryPriceTextMobile {
display: none;
}

View File

@@ -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<TotalPrice>(
(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,
}
)
}

View File

@@ -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}
/>
</form>
)}

View File

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