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 { useEffect, useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
|||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { calculateTotalPrice } from "./utils"
|
||||||
|
|
||||||
import styles from "./rateSummary.module.css"
|
import styles from "./rateSummary.module.css"
|
||||||
|
|
||||||
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
import type { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
@@ -22,6 +26,7 @@ export default function RateSummary({
|
|||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
packages,
|
packages,
|
||||||
roomsAvailability,
|
roomsAvailability,
|
||||||
|
rooms,
|
||||||
}: RateSummaryProps) {
|
}: RateSummaryProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
@@ -34,87 +39,107 @@ export default function RateSummary({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const selectedRateSummary = getSelectedRateSummary()
|
const selectedRateSummary = getSelectedRateSummary()
|
||||||
|
const totalRoomsRequired = rooms?.length || 1
|
||||||
if (selectedRateSummary.length === 0) return null
|
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(
|
const petRoomPackage = packages?.find(
|
||||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||||
)
|
)
|
||||||
|
|
||||||
const petRoomLocalPrice =
|
const totalPriceToShow = calculateTotalPrice(
|
||||||
isPetRoomSelected && petRoomPackage?.localPrice.totalPrice
|
selectedRateSummary,
|
||||||
? Number(petRoomPackage?.localPrice.totalPrice)
|
isUserLoggedIn,
|
||||||
: 0
|
petRoomPackage
|
||||||
const petRoomRequestedPrice =
|
)
|
||||||
isPetRoomSelected && petRoomPackage?.requestedPrice.totalPrice
|
const isAllRoomsSelected = selectedRateSummary.length === totalRoomsRequired
|
||||||
? 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 checkInDate = new Date(roomsAvailability.checkInDate)
|
const checkInDate = new Date(roomsAvailability.checkInDate)
|
||||||
const checkOutDate = new Date(roomsAvailability.checkOutDate)
|
const checkOutDate = new Date(roomsAvailability.checkOutDate)
|
||||||
const nights = dt(checkOutDate).diff(dt(checkInDate), "days")
|
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(
|
const summaryPriceText = `${intl.formatMessage(
|
||||||
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
{ id: "{totalNights, plural, one {# night} other {# nights}}" },
|
||||||
{ totalNights: nights }
|
{ totalNights: nights }
|
||||||
)}, ${intl.formatMessage(
|
)}, ${intl.formatMessage(
|
||||||
{ id: "{totalAdults, plural, one {# adult} other {# adults}}" },
|
{ 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(
|
? `, ${intl.formatMessage(
|
||||||
{ id: "{totalChildren, plural, one {# child} other {# children}}" },
|
{ 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 (
|
return (
|
||||||
<div className={styles.summary} data-visible={isVisible}>
|
<div className={styles.summary} data-visible={isVisible}>
|
||||||
{showMemberDiscountBanner && <SignupPromoMobile />}
|
{showMemberDiscountBanner && <SignupPromoMobile />}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.summaryText}>
|
<div className={styles.summaryText}>
|
||||||
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
|
{selectedRateSummary.map((room, index) => (
|
||||||
<Body color="uiTextMediumContrast">{`${priceName}, ${priceTerm}`}</Body>
|
<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>
|
||||||
<div className={styles.summaryPriceContainer}>
|
<div className={styles.summaryPriceContainer}>
|
||||||
{showMemberDiscountBanner && (
|
{showMemberDiscountBanner && (
|
||||||
<div className={styles.promoContainer}>
|
<div className={styles.promoContainer}>
|
||||||
<SignupPromoDesktop
|
<SignupPromoDesktop
|
||||||
memberPrice={{
|
memberPrice={{
|
||||||
amount: member.localPrice.pricePerStay + petRoomLocalPrice,
|
amount: selectedRateSummary.reduce((total, room) => {
|
||||||
currency: member.localPrice.currency,
|
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>
|
</div>
|
||||||
@@ -177,6 +202,7 @@ export default function RateSummary({
|
|||||||
type="submit"
|
type="submit"
|
||||||
theme="base"
|
theme="base"
|
||||||
className={styles.continueButton}
|
className={styles.continueButton}
|
||||||
|
disabled={!isAllRoomsSelected}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Continue" })}
|
{intl.formatMessage({ id: "Continue" })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -79,10 +79,13 @@
|
|||||||
}
|
}
|
||||||
.petInfo,
|
.petInfo,
|
||||||
.promoContainer,
|
.promoContainer,
|
||||||
.summaryText,
|
|
||||||
.summaryPriceTextDesktop {
|
.summaryPriceTextDesktop {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.summaryText {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
.summaryPriceTextMobile {
|
.summaryPriceTextMobile {
|
||||||
display: none;
|
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 RateSummary from "../RateSummary"
|
||||||
import { RoomSelectionPanel } from "../RoomSelectionPanel"
|
import { RoomSelectionPanel } from "../RoomSelectionPanel"
|
||||||
import SelectedRoomPanel from "../SelectedRoomPanel"
|
import SelectedRoomPanel from "../SelectedRoomPanel"
|
||||||
import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
|
|
||||||
import { roomSelectionPanelVariants } from "./variants"
|
import { roomSelectionPanelVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
@@ -25,7 +24,6 @@ import {
|
|||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
|
|
||||||
|
|
||||||
export default function Rooms({
|
export default function Rooms({
|
||||||
availablePackages,
|
availablePackages,
|
||||||
@@ -37,6 +35,7 @@ export default function Rooms({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
const hotelId = searchParams.get("hotel")
|
const hotelId = searchParams.get("hotel")
|
||||||
const arrivalDate = searchParams.get("fromDate")
|
const arrivalDate = searchParams.get("fromDate")
|
||||||
@@ -47,6 +46,7 @@ export default function Rooms({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
selectedPackagesByRoom,
|
selectedPackagesByRoom,
|
||||||
|
visibleRooms,
|
||||||
setVisibleRooms,
|
setVisibleRooms,
|
||||||
setRoomsAvailability,
|
setRoomsAvailability,
|
||||||
getFilteredRooms,
|
getFilteredRooms,
|
||||||
@@ -62,33 +62,10 @@ export default function Rooms({
|
|||||||
|
|
||||||
const isMultipleRooms = bookingWidgetSearchData.rooms.length > 1
|
const isMultipleRooms = bookingWidgetSearchData.rooms.length > 1
|
||||||
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeRates(bookingWidgetSearchData.rooms.length)
|
initializeRates(bookingWidgetSearchData.rooms.length)
|
||||||
}, [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(
|
const defaultPackages: DefaultFilterOptions[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -197,7 +174,9 @@ export default function Rooms({
|
|||||||
const SCROLL_OFFSET = 100
|
const SCROLL_OFFSET = 100
|
||||||
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
|
const roomElements = document.querySelectorAll(`.${styles.roomContainer}`)
|
||||||
const index = selectedRates.findIndex((rate) => rate === undefined)
|
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) {
|
if (selectedRoom) {
|
||||||
const elementPosition = selectedRoom.getBoundingClientRect().top
|
const elementPosition = selectedRoom.getBoundingClientRect().top
|
||||||
@@ -286,6 +265,7 @@ export default function Rooms({
|
|||||||
isUserLoggedIn={isUserLoggedIn}
|
isUserLoggedIn={isUserLoggedIn}
|
||||||
packages={availablePackages}
|
packages={availablePackages}
|
||||||
roomsAvailability={roomsAvailability}
|
roomsAvailability={roomsAvailability}
|
||||||
|
rooms={bookingWidgetSearchData.rooms}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
|
||||||
import type { RoomPackages } from "./roomFilter"
|
import type { RoomPackages } from "./roomFilter"
|
||||||
import type { Rate } from "./selectRate"
|
import type { Room } from "./selectRate"
|
||||||
|
|
||||||
export interface RateSummaryProps {
|
export interface RateSummaryProps {
|
||||||
isUserLoggedIn: boolean
|
isUserLoggedIn: boolean
|
||||||
packages: RoomPackages | undefined
|
packages: RoomPackages | undefined
|
||||||
roomsAvailability: RoomsAvailability
|
roomsAvailability: RoomsAvailability
|
||||||
|
rooms: Room[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user