chore(BOOK-739): replace caption with typography * chore(BOOK-739): replace caption with typography * chore(BOOK-739): refactor div * chore(BOOK-739): refactor badge * chore(BOOK-739): remove span * chore(BOOK-739): skeleton update * chore(BOOK-739): update * chore(BOOK-739): update reward * chore(BOOK-739): update voucher currency Approved-by: Erik Tiekstra
363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
import { cx } from "class-variance-authority"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { RateEnum } from "@scandic-hotels/common/constants/rate"
|
|
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
|
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
|
|
import { useIsLoggedIn } from "../../../../hooks/useIsLoggedIn"
|
|
import SignupPromoDesktop from "../../../SignupPromo/Desktop"
|
|
import { isSpecialRate } from "./utils"
|
|
|
|
import styles from "./rateSummary.module.css"
|
|
|
|
import type { useSelectRateContext } from "../../../../contexts/SelectRate/SelectRateContext"
|
|
import type { SelectedRate } from "../../../../contexts/SelectRate/types"
|
|
|
|
export function DesktopSummary({
|
|
input,
|
|
selectedRates,
|
|
isSubmitting,
|
|
}: {
|
|
selectedRates: ReturnType<typeof useSelectRateContext>["selectedRates"]
|
|
isSubmitting: boolean
|
|
input: ReturnType<typeof useSelectRateContext>["input"]
|
|
}) {
|
|
const intl = useIntl()
|
|
const isUserLoggedIn = useIsLoggedIn()
|
|
|
|
if (!selectedRates.totalPrice) {
|
|
return null
|
|
}
|
|
|
|
const hasMemberRates = selectedRates.rates.some(
|
|
(rate) => rate && "member" in rate && rate.member
|
|
)
|
|
const showMemberDiscountBanner = hasMemberRates && !isUserLoggedIn
|
|
|
|
const totalNights = intl.formatMessage(
|
|
{
|
|
id: "booking.numberOfNights",
|
|
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
|
},
|
|
{ totalNights: input.nights }
|
|
)
|
|
const totalAdults = intl.formatMessage(
|
|
{
|
|
id: "booking.numberOfAdults",
|
|
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
|
|
},
|
|
{
|
|
adults:
|
|
input.data?.booking.rooms.reduce((acc, room) => acc + room.adults, 0) ??
|
|
0,
|
|
}
|
|
)
|
|
const childrenInOneOrMoreRooms = input.data?.booking.rooms.some(
|
|
(room) => room.childrenInRoom?.length
|
|
)
|
|
const childrenInroom = intl.formatMessage(
|
|
{
|
|
id: "booking.numberOfChildren",
|
|
defaultMessage: "{children, plural, one {# child} other {# children}}",
|
|
},
|
|
{
|
|
children: input.data?.booking.rooms.reduce(
|
|
(acc, room) => acc + (room.childrenInRoom?.length ?? 0),
|
|
0
|
|
),
|
|
}
|
|
)
|
|
|
|
const totalChildren = childrenInOneOrMoreRooms ? `, ${childrenInroom}` : ""
|
|
const totalRooms = intl.formatMessage(
|
|
{
|
|
id: "booking.numberOfRooms",
|
|
defaultMessage: "{totalRooms, plural, one {# room} other {# rooms}}",
|
|
},
|
|
{ totalRooms: input.roomCount }
|
|
)
|
|
const summaryPriceText = `${totalNights}, ${totalAdults}${totalChildren}, ${totalRooms}`
|
|
const isAllRoomsSelected = selectedRates.state === "ALL_SELECTED"
|
|
|
|
const showDiscounted =
|
|
isUserLoggedIn || selectedRates.rates.some(isSpecialRate)
|
|
|
|
const mainRoomRate = selectedRates.rates.at(0)
|
|
const mainRoomCurrency = getRoomCurrency(mainRoomRate)
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.summaryText}>
|
|
{selectedRates.rates.map((room, index) => {
|
|
return (
|
|
<RateSummary
|
|
key={index}
|
|
room={room}
|
|
roomIndex={index}
|
|
isMultiRoom={selectedRates.rates.length > 1}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className={styles.summaryPriceContainer}>
|
|
{showMemberDiscountBanner && (
|
|
<div className={styles.promoContainer}>
|
|
<SignupPromoDesktop
|
|
memberPrice={{
|
|
amount: selectedRates.rates.reduce((total, rate) => {
|
|
if (!rate) {
|
|
return total
|
|
}
|
|
|
|
const memberExists = "member" in rate && rate.member
|
|
const publicExists = "public" in rate && rate.public
|
|
if (!memberExists && !publicExists) {
|
|
return total
|
|
}
|
|
const price =
|
|
rate.member?.localPrice.pricePerStay ||
|
|
rate.public?.localPrice.pricePerStay
|
|
if (!price) {
|
|
return total
|
|
}
|
|
const selectedPackagesPrice =
|
|
rate.roomInfo.selectedPackages.reduce(
|
|
(acc, pkg) => acc + pkg.localPrice.totalPrice,
|
|
0
|
|
)
|
|
return total + price + selectedPackagesPrice
|
|
}, 0),
|
|
currency: mainRoomCurrency ?? "",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className={styles.summaryPriceTextDesktop}>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "booking.totalPriceInclVat",
|
|
defaultMessage: "<b>Total price</b> (incl VAT)",
|
|
},
|
|
{ b: (str) => <b>{str}</b> }
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
<Typography
|
|
variant="Body/Supporting text (caption)/smRegular"
|
|
className={styles.secondaryText}
|
|
>
|
|
<p>{summaryPriceText}</p>
|
|
</Typography>
|
|
</div>
|
|
<div className={styles.summaryPrice}>
|
|
<div className={styles.summaryPriceTextDesktop}>
|
|
<Typography
|
|
variant="Title/Subtitle/md"
|
|
className={cx(styles.alignRight, {
|
|
[styles.red]: showDiscounted,
|
|
})}
|
|
>
|
|
<p>
|
|
{formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.local.price,
|
|
selectedRates.totalPrice.local.currency,
|
|
selectedRates.totalPrice.local.additionalPrice,
|
|
selectedRates.totalPrice.local.additionalPriceCurrency
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
{showDiscounted && selectedRates.totalPrice.local.regularPrice && (
|
|
<Typography
|
|
variant="Body/Supporting text (caption)/smRegular"
|
|
className={styles.regularPrice}
|
|
>
|
|
<p>
|
|
{formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.local.regularPrice,
|
|
selectedRates.totalPrice.local.currency
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
)}
|
|
{!!selectedRates.totalPrice.requested && (
|
|
<Typography
|
|
variant="Body/Paragraph/mdRegular"
|
|
className={styles.approxPrice}
|
|
>
|
|
<p>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "booking.approxValue",
|
|
defaultMessage: "Approx. {value}",
|
|
},
|
|
{
|
|
value: formatPrice(
|
|
intl,
|
|
selectedRates.totalPrice.requested.price,
|
|
selectedRates.totalPrice.requested.currency,
|
|
selectedRates.totalPrice.requested.additionalPrice,
|
|
selectedRates.totalPrice.requested
|
|
.additionalPriceCurrency
|
|
),
|
|
}
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
)}
|
|
</div>
|
|
<Button
|
|
className={styles.continueButton}
|
|
disabled={!isAllRoomsSelected || isSubmitting}
|
|
theme="base"
|
|
type="submit"
|
|
>
|
|
{intl.formatMessage({
|
|
id: "common.continue",
|
|
defaultMessage: "Continue",
|
|
})}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function RateSummary({
|
|
roomIndex,
|
|
room,
|
|
isMultiRoom,
|
|
}: {
|
|
room: SelectedRate | undefined
|
|
roomIndex: number
|
|
isMultiRoom: boolean
|
|
}) {
|
|
const intl = useIntl()
|
|
const getRateDetails = useRateDetails()
|
|
|
|
if (!room || !room.isSelected) {
|
|
return (
|
|
<div key={`unselected-${roomIndex}`}>
|
|
<Typography
|
|
variant="Title/Subtitle/md"
|
|
className={styles.placeholderText}
|
|
>
|
|
<p>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "booking.roomIndex",
|
|
defaultMessage: "Room {roomIndex}",
|
|
},
|
|
{ roomIndex: roomIndex + 1 }
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
<Typography
|
|
variant="Body/Paragraph/mdRegular"
|
|
className={styles.placeholderText}
|
|
>
|
|
<p>
|
|
{intl.formatMessage({
|
|
id: "booking.selectRoom",
|
|
defaultMessage: "Select room",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div key={roomIndex}>
|
|
{isMultiRoom ? (
|
|
<>
|
|
<Typography variant="Title/Subtitle/md">
|
|
<p>
|
|
{intl.formatMessage(
|
|
{ id: "booking.roomIndex", defaultMessage: "Room {roomIndex}" },
|
|
{ roomIndex: roomIndex + 1 }
|
|
)}
|
|
</p>
|
|
</Typography>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>{room.roomInfo.roomType}</p>
|
|
</Typography>
|
|
<Typography
|
|
variant="Body/Paragraph/mdRegular"
|
|
className={styles.secondaryText}
|
|
>
|
|
<p>{getRateDetails(room.rate)}</p>
|
|
</Typography>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Typography variant="Title/Subtitle/md">
|
|
<p>{room.roomInfo.roomType}</p>
|
|
</Typography>
|
|
<Typography
|
|
variant="Body/Paragraph/mdRegular"
|
|
className={styles.secondaryText}
|
|
>
|
|
<p>{getRateDetails(room.rate)}</p>
|
|
</Typography>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function useRateDetails() {
|
|
const intl = useIntl()
|
|
const freeCancelation = intl.formatMessage({
|
|
id: "booking.freeCancellation",
|
|
defaultMessage: "Free cancellation",
|
|
})
|
|
const nonRefundable = intl.formatMessage({
|
|
id: "booking.nonRefundable",
|
|
defaultMessage: "Non-refundable",
|
|
})
|
|
const freeBooking = intl.formatMessage({
|
|
id: "booking.freeRebooking",
|
|
defaultMessage: "Free rebooking",
|
|
})
|
|
const payLater = intl.formatMessage({
|
|
id: "booking.payLater",
|
|
defaultMessage: "Pay later",
|
|
})
|
|
const payNow = intl.formatMessage({
|
|
id: "booking.payNow",
|
|
defaultMessage: "Pay now",
|
|
})
|
|
|
|
return (rate: RateEnum) => {
|
|
switch (rate) {
|
|
case RateEnum.change:
|
|
return `${freeBooking}, ${payNow}`
|
|
case RateEnum.flex:
|
|
return `${freeCancelation}, ${payLater}`
|
|
case RateEnum.save:
|
|
default:
|
|
return `${nonRefundable}, ${payNow}`
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRoomCurrency(rate: SelectedRate | undefined) {
|
|
if (!rate) {
|
|
return null
|
|
}
|
|
|
|
if ("member" in rate && rate.member?.localPrice) {
|
|
return rate.member.localPrice.currency
|
|
}
|
|
|
|
if ("public" in rate && rate.public?.localPrice) {
|
|
return rate.public.localPrice.currency
|
|
}
|
|
}
|