Merged in feat/sw-2874-move-select-rate (pull request #2750)
Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
"use client"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
|
||||
import { longDateFormat } from "@scandic-hotels/common/constants/dateFormats"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useSelectRateContext } from "../../../../../../contexts/SelectRate/SelectRateContext"
|
||||
import useLang from "../../../../../../hooks/useLang"
|
||||
import PriceDetailsModal from "../../../../../PriceDetailsModal"
|
||||
import SignupPromoDesktop from "../../../../../SignupPromo/Desktop"
|
||||
import { useRateTitles } from "../../../Rooms/RoomsList/RoomListItem/Rates/useRateTitles"
|
||||
import { isBookingCodeRate } from "../../utils"
|
||||
import Room from "../Room"
|
||||
|
||||
import styles from "./summaryContent.module.css"
|
||||
|
||||
import type { Price } from "../../../../../../contexts/SelectRate/getTotalPrice"
|
||||
|
||||
export type SelectRateSummaryProps = {
|
||||
isMember: boolean
|
||||
bookingCode?: string
|
||||
toggleSummaryOpen: () => void
|
||||
}
|
||||
|
||||
export default function SummaryContent({
|
||||
isMember,
|
||||
toggleSummaryOpen,
|
||||
}: SelectRateSummaryProps) {
|
||||
const { selectedRates, input } = useSelectRateContext()
|
||||
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const rateTitles = useRateTitles()
|
||||
|
||||
const nightsLabel = intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{ totalNights: input.nights }
|
||||
)
|
||||
|
||||
const memberPrice =
|
||||
selectedRates.rates.length === 1 &&
|
||||
selectedRates.rates[0] &&
|
||||
"member" in selectedRates.rates[0]
|
||||
? selectedRates.rates[0].member
|
||||
: null
|
||||
|
||||
const containsBookingCodeRate = selectedRates.rates.find(
|
||||
(r) => r && isBookingCodeRate(r)
|
||||
)
|
||||
|
||||
if (!selectedRates?.totalPrice) {
|
||||
return null
|
||||
}
|
||||
|
||||
const showDiscounted = containsBookingCodeRate || isMember
|
||||
const totalRegularPrice = selectedRates?.totalPrice?.local?.regularPrice
|
||||
? selectedRates.totalPrice.local.regularPrice
|
||||
: 0
|
||||
|
||||
const showStrikeThroughPrice =
|
||||
totalRegularPrice > selectedRates?.totalPrice?.local?.price
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
<header>
|
||||
<div className={styles.headingWrapper}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h3 className={styles.heading}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Booking summary",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<IconButton
|
||||
className={styles.closeButton}
|
||||
onPress={toggleSummaryOpen}
|
||||
theme="Black"
|
||||
style="Muted"
|
||||
>
|
||||
<MaterialIcon
|
||||
icon="keyboard_arrow_down"
|
||||
size={20}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p className={styles.dates}>
|
||||
{dt(input.data?.booking.fromDate)
|
||||
.locale(lang)
|
||||
.format(longDateFormat[lang])}
|
||||
<MaterialIcon icon="arrow_forward" size={15} color="CurrentColor" />
|
||||
{dt(input.data?.booking.toDate)
|
||||
.locale(lang)
|
||||
.format(longDateFormat[lang])}{" "}
|
||||
({nightsLabel})
|
||||
</p>
|
||||
</Typography>
|
||||
</header>
|
||||
|
||||
<Divider color="Border/Divider/Subtle" />
|
||||
|
||||
{selectedRates.rates.map((room, idx) => {
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Room
|
||||
key={idx}
|
||||
room={mapToRoom({
|
||||
isMember,
|
||||
rate: room,
|
||||
input,
|
||||
idx,
|
||||
getPriceForRoom: selectedRates.getPriceForRoom,
|
||||
rateTitles,
|
||||
})}
|
||||
roomNumber={idx + 1}
|
||||
roomCount={selectedRates.rates.length}
|
||||
isMember={isMember}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "<b>Total price</b> (incl VAT)",
|
||||
},
|
||||
{
|
||||
b: (str) => (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{str}</span>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
{selectedRates.totalPrice.requested ? (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.approxPrice}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "Approx. {value}",
|
||||
},
|
||||
{
|
||||
value: formatPrice(
|
||||
intl,
|
||||
selectedRates.totalPrice.requested.price,
|
||||
selectedRates.totalPrice.requested.currency,
|
||||
selectedRates.totalPrice.requested.additionalPrice,
|
||||
selectedRates.totalPrice.requested
|
||||
.additionalPriceCurrency
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.prices}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span
|
||||
className={cx(styles.price, {
|
||||
[styles.discounted]: showDiscounted,
|
||||
})}
|
||||
data-testid="total-price"
|
||||
>
|
||||
{formatPrice(
|
||||
intl,
|
||||
selectedRates.totalPrice.local.price,
|
||||
selectedRates.totalPrice.local.currency,
|
||||
selectedRates.totalPrice.local.additionalPrice,
|
||||
selectedRates.totalPrice.local.additionalPriceCurrency
|
||||
)}
|
||||
</span>
|
||||
</Typography>
|
||||
{showDiscounted &&
|
||||
showStrikeThroughPrice &&
|
||||
selectedRates.totalPrice.local.regularPrice ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<s className={styles.strikeThroughRate}>
|
||||
{formatPrice(
|
||||
intl,
|
||||
selectedRates.totalPrice.local.regularPrice,
|
||||
selectedRates.totalPrice.local.currency
|
||||
)}
|
||||
</s>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PriceDetailsModal
|
||||
bookingCode={input.bookingCode}
|
||||
defaultCurrency={
|
||||
selectedRates.totalPrice.requested?.currency ??
|
||||
selectedRates.totalPrice.local.currency
|
||||
}
|
||||
rooms={selectedRates.rates
|
||||
.map((room, idx) => {
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
|
||||
const mapped = mapToRoom({
|
||||
isMember,
|
||||
rate: room,
|
||||
input,
|
||||
idx,
|
||||
getPriceForRoom: selectedRates.getPriceForRoom,
|
||||
rateTitles,
|
||||
})
|
||||
|
||||
function getPrice(
|
||||
room: NonNullable<(typeof selectedRates.rates)[number]>,
|
||||
isMember: boolean
|
||||
) {
|
||||
switch (room.type) {
|
||||
case "regular":
|
||||
return {
|
||||
regular: isMember
|
||||
? (room.member?.localPrice ?? room.public?.localPrice)
|
||||
: room.public?.localPrice,
|
||||
}
|
||||
case "campaign":
|
||||
return {
|
||||
campaign: isMember
|
||||
? (room.member ?? room.public)
|
||||
: room.public,
|
||||
}
|
||||
case "redemption":
|
||||
return {
|
||||
redemption: room.redemption,
|
||||
}
|
||||
case "code": {
|
||||
if ("corporateCheque" in room) {
|
||||
return {
|
||||
corporateCheque: room.corporateCheque,
|
||||
}
|
||||
}
|
||||
|
||||
if ("voucher" in room) {
|
||||
return {
|
||||
voucher: room.voucher,
|
||||
}
|
||||
}
|
||||
if ("public" in room) {
|
||||
return {
|
||||
regular: isMember
|
||||
? (room.member?.localPrice ?? room.public?.localPrice)
|
||||
: room.public?.localPrice,
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown price type")
|
||||
}
|
||||
}
|
||||
|
||||
const p = getPrice(room!, isMember)
|
||||
|
||||
return {
|
||||
...mapped,
|
||||
idx,
|
||||
getPriceForRoom: selectedRates.getPriceForRoom,
|
||||
rateTitles,
|
||||
price: p,
|
||||
bedType: undefined,
|
||||
breakfast: undefined,
|
||||
breakfastIncluded:
|
||||
room?.rateDefinition.breakfastIncluded ?? false,
|
||||
rateDefinition: room.rateDefinition,
|
||||
}
|
||||
})
|
||||
.filter((x) => !!x)}
|
||||
fromDate={input.data?.booking.fromDate ?? ""}
|
||||
toDate={input.data?.booking.toDate ?? ""}
|
||||
totalPrice={selectedRates.totalPrice}
|
||||
vat={selectedRates.vat}
|
||||
/>
|
||||
</div>
|
||||
{!isMember && memberPrice ? (
|
||||
<SignupPromoDesktop
|
||||
memberPrice={{
|
||||
amount: memberPrice.localPrice.pricePerStay,
|
||||
currency: memberPrice.localPrice.currency,
|
||||
}}
|
||||
badgeContent={"✌️"}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function mapToRoom({
|
||||
isMember,
|
||||
rate,
|
||||
input,
|
||||
idx,
|
||||
getPriceForRoom,
|
||||
rateTitles,
|
||||
}: {
|
||||
isMember: boolean
|
||||
rate: NonNullable<
|
||||
ReturnType<typeof useSelectRateContext>["selectedRates"]["rates"][number]
|
||||
>
|
||||
input: ReturnType<typeof useSelectRateContext>["input"]
|
||||
idx: number
|
||||
getPriceForRoom: (roomIndex: number) => Price | null
|
||||
rateTitles: ReturnType<typeof useRateTitles>
|
||||
}) {
|
||||
return {
|
||||
adults: input.data?.booking.rooms[idx].adults || 0,
|
||||
childrenInRoom: input.data?.booking.rooms[idx].childrenInRoom,
|
||||
roomType: rate.roomInfo.roomType,
|
||||
roomRate: rate,
|
||||
cancellationText: rateTitles[rate.rate].title,
|
||||
roomPrice: {
|
||||
perNight: { local: { price: -1, currency: CurrencyEnum.SEK } },
|
||||
perStay: getPriceForRoom(idx) ?? {
|
||||
local: { price: -1, currency: CurrencyEnum.Unknown },
|
||||
},
|
||||
},
|
||||
rateDetails: isMember
|
||||
? (rate.rateDefinitionMember?.generalTerms ??
|
||||
rate.rateDefinition.generalTerms)
|
||||
: rate.rateDefinition.generalTerms,
|
||||
packages: rate.roomInfo.selectedPackages,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.summary {
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
display: grid;
|
||||
gap: var(--Space-x2);
|
||||
padding: var(--Space-x3);
|
||||
}
|
||||
|
||||
.headingWrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
margin-top: -10px; /* Compensate for padding of the button */
|
||||
margin-right: -10px; /* Compensate for padding of the button */
|
||||
}
|
||||
|
||||
.dates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
justify-content: flex-start;
|
||||
color: var(--Text-Accent-Secondary);
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--Space-x15);
|
||||
}
|
||||
|
||||
.prices {
|
||||
justify-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: var(--Text-Default);
|
||||
|
||||
&.discounted {
|
||||
color: var(--Text-Accent-Primary);
|
||||
}
|
||||
}
|
||||
|
||||
.strikeThroughRate {
|
||||
text-decoration: line-through;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
.approxPrice {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
Reference in New Issue
Block a user