fix(SW-3616): Handle EuroBonus point type everywhere * Add tests to formatPrice * formatPrice * More work replacing config with api points type * More work replacing config with api points type * More fixing with currency * maybe actually fixed it * Fix MyStay * Clean up * Fix comments * Merge branch 'master' into fix/refactor-currency-display * Fix calculateTotalPrice for EB points + SF points + cash Approved-by: Joakim Jäderberg
216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { IconButton } from "../../../IconButton"
|
|
import { Typography } from "../../../Typography"
|
|
|
|
import { HotelCardDialogImage } from "../../HotelCardDialogImage"
|
|
import { NoPriceAvailableCard } from "../../NoPriceAvailableCard"
|
|
|
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
|
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
|
import { useUrlWithSearchParam } from "@scandic-hotels/common/hooks/useUrlWithSearchParam"
|
|
import ButtonLink from "../../../ButtonLink"
|
|
import { FacilityToIcon } from "../../../FacilityToIcon"
|
|
import { HotelPin } from "../../../Map/types"
|
|
import { HotelPointsRow } from "../../HotelPointsRow"
|
|
import { RoomPrice } from "../../RoomPrice"
|
|
import styles from "./standaloneHotelCardDialog.module.css"
|
|
|
|
interface StandaloneHotelCardProps {
|
|
lang: Lang
|
|
data: HotelPin
|
|
isUserLoggedIn: boolean
|
|
handleClose: () => void
|
|
onClick?: () => void
|
|
}
|
|
|
|
export function StandaloneHotelCardDialog({
|
|
data,
|
|
lang,
|
|
handleClose,
|
|
isUserLoggedIn,
|
|
onClick,
|
|
}: StandaloneHotelCardProps) {
|
|
const intl = useIntl()
|
|
const [imageError, setImageError] = useState(false)
|
|
const {
|
|
name,
|
|
chequePrice,
|
|
publicPrice,
|
|
memberPrice,
|
|
redemptionPrice,
|
|
pointsType,
|
|
voucherPrice,
|
|
currency,
|
|
amenities,
|
|
image,
|
|
ratings,
|
|
operaId,
|
|
hasEnoughPoints,
|
|
} = data
|
|
|
|
const notEnoughPointsLabel = intl.formatMessage({
|
|
id: "booking.notEnoughPoints",
|
|
defaultMessage: "Not enough points",
|
|
})
|
|
|
|
const shouldShowNotEnoughPoints = redemptionPrice && !hasEnoughPoints
|
|
const selectRateUrl = useUrlWithSearchParam(
|
|
{ key: "hotel", value: operaId },
|
|
selectRate(lang)
|
|
)
|
|
const showPriceCard = !!(
|
|
publicPrice ||
|
|
memberPrice ||
|
|
redemptionPrice ||
|
|
voucherPrice ||
|
|
chequePrice
|
|
)
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<IconButton
|
|
variant="Muted"
|
|
emphasis
|
|
className={styles.closeButton}
|
|
onPress={handleClose}
|
|
aria-label={intl.formatMessage({
|
|
id: "common.close",
|
|
defaultMessage: "Close",
|
|
})}
|
|
iconName="close"
|
|
/>
|
|
<HotelCardDialogImage
|
|
imageSrc={image?.url}
|
|
altText={image?.alt}
|
|
rating={{ tripAdvisor: ratings?.tripAdvisor ?? null }}
|
|
imageError={imageError}
|
|
setImageError={setImageError}
|
|
position="left"
|
|
/>
|
|
<div className={styles.content}>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<h4 className={styles.name}>{name}</h4>
|
|
</Typography>
|
|
<div className={styles.facilities}>
|
|
{amenities.slice(0, 3).map((facility) => {
|
|
const Icon = (
|
|
<FacilityToIcon id={facility.id} size={16} color="Icon/Default" />
|
|
)
|
|
return (
|
|
<div className={styles.facilitiesItem} key={facility.id}>
|
|
{Icon && Icon}
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<span>{facility.name}</span>
|
|
</Typography>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
{showPriceCard ? (
|
|
<>
|
|
<div className={styles.priceCard}>
|
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
<p>
|
|
{redemptionPrice
|
|
? intl.formatMessage({
|
|
id: "hotelCard.availableRates",
|
|
defaultMessage: "Available rates",
|
|
})
|
|
: intl.formatMessage({
|
|
id: "common.from",
|
|
defaultMessage: "From",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
{chequePrice ? (
|
|
<RoomPrice
|
|
price={chequePrice.numberOfCheques}
|
|
currency="CC"
|
|
includePerNight={false}
|
|
>
|
|
{chequePrice.additionalPricePerStay > 0 ? (
|
|
<>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
<span> + </span>
|
|
</Typography>
|
|
<Typography variant="Title/Subtitle/md">
|
|
<span>{chequePrice.additionalPricePerStay}</span>
|
|
</Typography>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<span> {chequePrice.currency}</span>
|
|
</Typography>
|
|
</>
|
|
) : null}
|
|
</RoomPrice>
|
|
) : null}
|
|
{voucherPrice ? (
|
|
<RoomPrice
|
|
price={voucherPrice}
|
|
currency={intl.formatMessage(
|
|
{
|
|
id: "price.numberOfVouchers",
|
|
defaultMessage:
|
|
"{numberOfVouchers, plural, one {Voucher} other {Vouchers}}",
|
|
},
|
|
{
|
|
numberOfVouchers: voucherPrice,
|
|
}
|
|
)}
|
|
includePerNight={false}
|
|
/>
|
|
) : null}
|
|
{/* Show public price if:
|
|
1) user is not logged in, or
|
|
2) user is logged in but no member price is available (use of booking codes)
|
|
*/}
|
|
{publicPrice && (!isUserLoggedIn || !memberPrice) ? (
|
|
<RoomPrice price={publicPrice} currency={currency} />
|
|
) : null}
|
|
{memberPrice ? (
|
|
<RoomPrice
|
|
className={styles.memberPrice}
|
|
price={memberPrice}
|
|
currency={currency}
|
|
/>
|
|
) : null}
|
|
{redemptionPrice ? (
|
|
<HotelPointsRow
|
|
pointsPerStay={redemptionPrice}
|
|
pointsType={pointsType}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
{shouldShowNotEnoughPoints ? (
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<div className={styles.notEnoughPointsButton}>
|
|
{notEnoughPointsLabel}
|
|
</div>
|
|
</Typography>
|
|
) : (
|
|
<ButtonLink
|
|
href={selectRateUrl}
|
|
variant="Primary"
|
|
color="Primary"
|
|
size="sm"
|
|
onClick={onClick}
|
|
>
|
|
{intl.formatMessage({
|
|
id: "common.seeRooms",
|
|
defaultMessage: "See rooms",
|
|
})}
|
|
</ButtonLink>
|
|
)}
|
|
</>
|
|
) : (
|
|
<NoPriceAvailableCard />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|