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:
@@ -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>
|
||||
|
||||
@@ -79,10 +79,13 @@
|
||||
}
|
||||
.petInfo,
|
||||
.promoContainer,
|
||||
.summaryText,
|
||||
.summaryPriceTextDesktop {
|
||||
display: block;
|
||||
}
|
||||
.summaryText {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
.summaryPriceTextMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
58
components/HotelReservation/SelectRate/RateSummary/utils.ts
Normal file
58
components/HotelReservation/SelectRate/RateSummary/utils.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user