fix(BOOK-418): Refactored StandaloneHotelCardDialog and fixed pricing issue when using redemption or booking codes
Approved-by: Bianca Widstam
This commit is contained in:
@@ -33,6 +33,7 @@ export function HotelCardDialogImage({
|
||||
src={imageSrc}
|
||||
alt={altText || ''}
|
||||
fill
|
||||
sizes={position === 'top' ? '200px' : '450px'}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { StandaloneHotelCardDialog } from './index'
|
||||
|
||||
import { Lang } from '@scandic-hotels/common/constants/language'
|
||||
import { fn } from 'storybook/test'
|
||||
import { hotelPins } from '../../../Map/InteractiveMap/storybookData'
|
||||
|
||||
const meta: Meta<typeof StandaloneHotelCardDialog> = {
|
||||
title: 'Components/StandaloneHotelCardDialog',
|
||||
@@ -15,29 +17,10 @@ type Story = StoryObj<typeof StandaloneHotelCardDialog>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
data: {
|
||||
name: 'Hotel Name',
|
||||
image: {
|
||||
url: 'img/img2.jpg',
|
||||
alt: 'Alt text',
|
||||
},
|
||||
coordinates: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
chequePrice: null,
|
||||
publicPrice: 100,
|
||||
memberPrice: 200,
|
||||
redemptionPrice: null,
|
||||
voucherPrice: null,
|
||||
rateType: null,
|
||||
currency: 'SEK',
|
||||
amenities: [],
|
||||
ratings: { tripAdvisor: 5 },
|
||||
operaId: '123',
|
||||
facilityIds: [],
|
||||
hasEnoughPoints: false,
|
||||
},
|
||||
lang: Lang.en,
|
||||
isUserLoggedIn: false,
|
||||
onClick: fn(),
|
||||
data: hotelPins[0],
|
||||
handleClose: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,30 +3,27 @@
|
||||
import { useState } from 'react'
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { selectRate } from '@scandic-hotels/common/constants/routes/hotelReservation'
|
||||
import Body from '../../../Body'
|
||||
import Caption from '../../../Caption'
|
||||
import Footnote from '../../../Footnote'
|
||||
import { IconButton } from '../../../IconButton'
|
||||
import { MaterialIcon } from '../../../Icons/MaterialIcon'
|
||||
import Link from '../../../Link'
|
||||
import { OldDSButton as Button } from '../../../OldDSButton'
|
||||
import Subtitle from '../../../Subtitle'
|
||||
import { Typography } from '../../../Typography'
|
||||
|
||||
import { HotelCardDialogImage } from '../../HotelCardDialogImage'
|
||||
import { NoPriceAvailableCard } from '../../NoPriceAvailableCard'
|
||||
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
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'
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
|
||||
interface StandaloneHotelCardProps {
|
||||
data: HotelPin
|
||||
lang: Lang
|
||||
data: HotelPin
|
||||
isUserLoggedIn: boolean
|
||||
handleClose: () => void
|
||||
onClick?: () => void
|
||||
@@ -63,6 +60,17 @@ export function StandaloneHotelCardDialog({
|
||||
})
|
||||
|
||||
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}>
|
||||
@@ -86,9 +94,9 @@ export function StandaloneHotelCardDialog({
|
||||
position="left"
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.name}>
|
||||
<Body textTransform="bold">{name}</Body>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h4 className={styles.name}>{name}</h4>
|
||||
</Typography>
|
||||
<div className={styles.facilities}>
|
||||
{amenities.slice(0, 3).map((facility) => {
|
||||
const Icon = (
|
||||
@@ -96,173 +104,93 @@ export function StandaloneHotelCardDialog({
|
||||
)
|
||||
return (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
{Icon}
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{facility.name}
|
||||
</Footnote>
|
||||
{Icon && Icon}
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>{facility.name}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.pricesContainer}>
|
||||
{publicPrice ||
|
||||
memberPrice ||
|
||||
redemptionPrice ||
|
||||
voucherPrice ||
|
||||
chequePrice ? (
|
||||
<>
|
||||
<div className={styles.priceCard}>
|
||||
{redemptionPrice ? (
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'Available rates',
|
||||
})}
|
||||
</Caption>
|
||||
) : (
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
</Caption>
|
||||
)}
|
||||
{chequePrice && (
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: chequePrice.numberOfCheques,
|
||||
currency: 'CC',
|
||||
}
|
||||
)}
|
||||
{chequePrice.additionalPricePerStay > 0
|
||||
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
' + ' +
|
||||
intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: chequePrice.additionalPricePerStay,
|
||||
currency: chequePrice.currency,
|
||||
}
|
||||
)
|
||||
: null}
|
||||
<Body asChild>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
{voucherPrice && (
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: voucherPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
{publicPrice && !isUserLoggedIn && (
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: publicPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
{memberPrice && (
|
||||
<Subtitle type="two" color="red">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: '{price} {currency}',
|
||||
},
|
||||
{
|
||||
price: memberPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild color="red">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>
|
||||
/
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'night',
|
||||
})}
|
||||
</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
{redemptionPrice && (
|
||||
<HotelPointsRow
|
||||
pointsPerStay={redemptionPrice}
|
||||
pointsCurrency={pointsCurrency}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{shouldShowNotEnoughPoints ? (
|
||||
<div className={styles.notEnoughPointsButton}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{notEnoughPointsLabel}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
onClick={onClick}
|
||||
{showPriceCard ? (
|
||||
<>
|
||||
<div className={styles.priceCard}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{redemptionPrice
|
||||
? intl.formatMessage({ defaultMessage: 'Available rates' })
|
||||
: intl.formatMessage({ defaultMessage: 'From' })}
|
||||
</p>
|
||||
</Typography>
|
||||
{chequePrice ? (
|
||||
<RoomPrice
|
||||
price={chequePrice.numberOfCheques}
|
||||
currency="CC"
|
||||
includePerNight={false}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'See rooms',
|
||||
})}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
{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={currency} />
|
||||
) : 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}
|
||||
pointsCurrency={pointsCurrency}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{shouldShowNotEnoughPoints ? (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<div className={styles.notEnoughPointsButton}>
|
||||
{notEnoughPointsLabel}
|
||||
</div>
|
||||
</Typography>
|
||||
) : (
|
||||
<ButtonLink
|
||||
href={selectRateUrl}
|
||||
variant="Primary"
|
||||
color="Primary"
|
||||
size="Small"
|
||||
onClick={onClick}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'See rooms',
|
||||
})}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
.container {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: var(--Base-Surface-Primary-light-Normal);
|
||||
display: flex;
|
||||
background-color: var(--Surface-Primary-Default);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: var(--Space-x05);
|
||||
right: var(--Space-x05);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
width: 225px;
|
||||
padding: var(--Space-x15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 48px;
|
||||
max-width: 180px;
|
||||
margin-bottom: var(--Space-x05);
|
||||
width: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: var(--Space-x1);
|
||||
}
|
||||
|
||||
.facilities {
|
||||
@@ -32,40 +37,23 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
justify-content: space-between;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
.priceCard {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
padding: var(--Space-x05) var(--Space-x1);
|
||||
background: var(--Base-Surface-Secondary-light-Normal);
|
||||
margin-top: var(--Space-x1);
|
||||
}
|
||||
|
||||
.pricesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
justify-content: space-between;
|
||||
.memberPrice {
|
||||
color: var(--Text-Accent-Primary);
|
||||
}
|
||||
|
||||
.content .button {
|
||||
.content .seeRoomsButton {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.notEnoughPointsButton {
|
||||
border-radius: var(--Corner-radius-rounded);
|
||||
border-width: 2px;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
.poinstRow {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: baseline;
|
||||
.roomPrice {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import Caption from '../../Caption'
|
||||
import Subtitle from '../../Subtitle'
|
||||
import { RoomPrice } from '../../HotelCard/RoomPrice'
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
import styles from './hotelPointsRow.module.css'
|
||||
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
|
||||
import styles from './hotelPointsRow.module.css'
|
||||
|
||||
export type PointsRowProps = {
|
||||
pointsPerStay: number
|
||||
@@ -21,27 +21,28 @@ export function HotelPointsRow({
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.poinstRow}>
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{pointsPerStay}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{pointsCurrency ??
|
||||
intl.formatMessage({
|
||||
defaultMessage: 'Points',
|
||||
})}
|
||||
</Caption>
|
||||
<RoomPrice
|
||||
className={styles.roomPrice}
|
||||
price={pointsPerStay}
|
||||
currency={
|
||||
pointsCurrency ?? intl.formatMessage({ defaultMessage: 'Points' })
|
||||
}
|
||||
includePerNight={false}
|
||||
>
|
||||
{additionalPricePerStay ? (
|
||||
<>
|
||||
{'+'}
|
||||
<Subtitle type="two" color="uiTextHighContrast">
|
||||
{additionalPricePerStay}
|
||||
</Subtitle>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{additionalPriceCurrency}
|
||||
</Caption>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span> + </span>
|
||||
</Typography>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<span>{additionalPricePerStay}</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span> {additionalPriceCurrency}</span>
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</RoomPrice>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useIntl } from 'react-intl'
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
interface RoomPriceProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
price: number
|
||||
currency: string
|
||||
includePerNight?: boolean
|
||||
}
|
||||
|
||||
export function RoomPrice({
|
||||
price,
|
||||
currency,
|
||||
children,
|
||||
includePerNight = true,
|
||||
...props
|
||||
}: RoomPriceProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<p {...props}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<span>{price}</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span> {currency}</span>
|
||||
</Typography>
|
||||
{children}
|
||||
{includePerNight ? (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<span>/{intl.formatMessage({ defaultMessage: 'night' })}</span>
|
||||
</Typography>
|
||||
) : null}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user