Merged in feat/BOOK-485-campaign-rate-my-stay (pull request #3120)

feat(BOOK-485): add campaign tag on my stay and update design

* feat(BOOK-485): add campaign tag on my stay and update design

* feat(BOOK-485): update rightAligned


Approved-by: Erik Tiekstra
This commit is contained in:
Bianca Widstam
2025-11-12 08:19:24 +00:00
parent c8cc4138b5
commit 2c044de187
12 changed files with 102 additions and 186 deletions

View File

@@ -37,6 +37,7 @@ export default function PriceDetails() {
totalPrice={totalPrice} totalPrice={totalPrice}
vat={bookedRoom.vatPercentage} vat={bookedRoom.vatPercentage}
defaultCurrency={bookedRoom.currencyCode} defaultCurrency={bookedRoom.currencyCode}
isCampaignRate={bookedRoom.isCampaignRate}
/> />
</div> </div>
) )

View File

@@ -1,5 +1,9 @@
.row { .row {
align-items: center;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
&.rightAligned {
justify-content: flex-end;
}
} }

View File

@@ -1,8 +1,8 @@
"use client" "use client"
import { cx } from "class-variance-authority"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import IconChip from "@scandic-hotels/design-system/IconChip" import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay" import { useMyStayStore } from "@/stores/my-stay"
@@ -11,31 +11,40 @@ import styles from "./bookingCode.module.css"
export default function BookingCode() { export default function BookingCode() {
const intl = useIntl() const intl = useIntl()
const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode) const { bookingCode, isCampaignRate } = useMyStayStore((state) => ({
bookingCode: state.bookedRoom.bookingCode,
isCampaignRate: state.bookedRoom.isCampaignRate,
}))
if (!bookingCode) { if (!bookingCode && !isCampaignRate) {
return null return null
} }
return ( const codeType = isCampaignRate
<div className={styles.row}> ? intl.formatMessage({
<Typography variant="Body/Supporting text (caption)/smRegular"> id: "booking.campaignCode",
<p> defaultMessage: "Campaign code",
{intl.formatMessage({ })
id: "booking.bookingCode", : intl.formatMessage({
defaultMessage: "Booking code", id: "booking.bookingCode",
})} defaultMessage: "Booking code",
</p> })
</Typography>
<IconChip const showCodeType = bookingCode || !isCampaignRate
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />} return (
> <div className={cx(styles.row, { [styles.rightAligned]: !showCodeType })}>
{showCodeType && (
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">
<span>{bookingCode}</span> <p>{codeType}</p>
</Typography> </Typography>
</IconChip> )}
<BookingCodeChip
bookingCode={bookingCode}
isCampaign={isCampaignRate}
withText={!showCodeType}
/>
</div> </div>
) )
} }

View File

@@ -1,42 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import IconChip from "@scandic-hotels/design-system/IconChip"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
export default function BookingCode() {
const intl = useIntl()
const bookingCode = useMyStayStore((state) => state.bookedRoom.bookingCode)
if (!bookingCode) {
return null
}
return (
<Typography variant="Body/Supporting text (caption)/smBold">
<IconChip
color="blue"
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
id: "booking.bookingCodeWithValue",
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)
}

View File

@@ -1,12 +1,19 @@
import BookingCode from "./BookingCode" import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import { useMyStayStore } from "@/stores/my-stay"
import PriceDetails from "./PriceDetails" import PriceDetails from "./PriceDetails"
import styles from "./information.module.css" import styles from "./information.module.css"
export default function BookingInformation() { export default function BookingInformation() {
const { bookingCode, isCampaignRate } = useMyStayStore((state) => ({
bookingCode: state.bookedRoom.bookingCode,
isCampaignRate: state.bookedRoom.isCampaignRate,
}))
return ( return (
<div className={styles.bookingInformation}> <div className={styles.bookingInformation}>
<BookingCode /> <BookingCodeChip bookingCode={bookingCode} isCampaign={isCampaignRate} />
<PriceDetails /> <PriceDetails />
</div> </div>
) )

View File

@@ -138,6 +138,7 @@ export function mapRoomDetails({
return { return {
...booking, ...booking,
isCampaignRate: booking.rateDefinition.isCampaignRate,
bedType: { bedType: {
description: room?.bedType.mainBed.description ?? "", description: room?.bedType.mainBed.description ?? "",
roomTypeCode: room?.bedType.code ?? "", roomTypeCode: room?.bedType.code ?? "",

View File

@@ -7,8 +7,8 @@ import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting" import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import Accordion from "@scandic-hotels/design-system/Accordion" import Accordion from "@scandic-hotels/design-system/Accordion"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem" import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import { BookingCodeChip } from "@scandic-hotels/design-system/BookingCodeChip"
import IconChip from "@scandic-hotels/design-system/IconChip" import IconChip from "@scandic-hotels/design-system/IconChip"
import DiscountIcon from "@scandic-hotels/design-system/Icons/DiscountIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import ImageGallery from "@scandic-hotels/design-system/ImageGallery" import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -422,29 +422,10 @@ export default function BookedRoomSidePeekContent({
</div> </div>
</div> </div>
</div> </div>
{bookingCode && ( <BookingCodeChip
<Typography variant="Body/Supporting text (caption)/smBold"> bookingCode={bookingCode}
<IconChip isCampaign={rateDefinition.isCampaignRate}
color="blue" />
icon={<DiscountIcon color="Icon/Feedback/Information" />}
>
{intl.formatMessage(
{
id: "booking.bookingCodeWithValue",
defaultMessage: "<strong>Booking code</strong>: {value}",
},
{
value: bookingCode,
strong: (text) => (
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>{text}</strong>
</Typography>
),
}
)}
</IconChip>
</Typography>
)}
<GuestDetails <GuestDetails
refId={refId} refId={refId}

View File

@@ -18,6 +18,7 @@ export type Room = Omit<
BookingConfirmation["booking"], BookingConfirmation["booking"],
"packages" | "roomPrice" "packages" | "roomPrice"
> & { > & {
isCampaignRate: boolean
bedType: BedTypeSchema bedType: BedTypeSchema
breakfast: Omit<BreakfastPackage, "requestedPrice"> | undefined | false breakfast: Omit<BreakfastPackage, "requestedPrice"> | undefined | false
breakfastChildren: Omit<BreakfastPackage, "requestedPrice"> | null breakfastChildren: Omit<BreakfastPackage, "requestedPrice"> | null

View File

@@ -16,7 +16,6 @@ export function RemoveBookingCodeButton() {
return ( return (
<BookingCodeChip <BookingCodeChip
bookingCode={bookingCode} bookingCode={bookingCode}
filledIcon
isCampaign={hasCampaignRates} isCampaign={hasCampaignRates}
withCloseButton={true} withCloseButton={true}
withText={false} withText={false}

View File

@@ -23,11 +23,6 @@ export const WithoutText: Story = {
render: () => <BookingCodeChip bookingCode="ABC123" withText={false} />, render: () => <BookingCodeChip bookingCode="ABC123" withText={false} />,
} }
export const FilledIcon: Story = {
args: {},
render: () => <BookingCodeChip bookingCode="ABC123" filledIcon />,
}
export const Unavailable: Story = { export const Unavailable: Story = {
args: {}, args: {},
render: () => <BookingCodeChip bookingCode="ABC123" isUnavailable />, render: () => <BookingCodeChip bookingCode="ABC123" isUnavailable />,
@@ -50,9 +45,7 @@ export const CampaignWithoutBookingCode: Story = {
render: () => <BookingCodeChip isCampaign />, render: () => <BookingCodeChip isCampaign />,
} }
export const CampaignFilledIcon: Story = { export const CampaignWithBookingCode: Story = {
args: {}, args: {},
render: () => ( render: () => <BookingCodeChip isCampaign bookingCode="SUMMER25" />,
<BookingCodeChip isCampaign bookingCode="SUMMER25" filledIcon />
),
} }

View File

@@ -7,6 +7,12 @@
text-decoration: line-through; text-decoration: line-through;
} }
.separator {
text-decoration: none;
display: inline-block;
margin-right: var(--Space-x05);
}
.center { .center {
justify-content: center; justify-content: center;
} }

View File

@@ -1,7 +1,6 @@
import { useIntl } from 'react-intl' import { useIntl } from 'react-intl'
import IconChip from '../IconChip' import IconChip from '../IconChip'
import DiscountIcon from '../Icons/Nucleo/Benefits/discount-2-2'
import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount' import FilledDiscountIcon from '../Icons/Nucleo/Benefits/FilledDiscount'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography' import { Typography } from '../Typography'
@@ -17,7 +16,6 @@ type BaseBookingCodeChipProps = {
isUnavailable?: boolean isUnavailable?: boolean
isCampaignUnavailable?: boolean isCampaignUnavailable?: boolean
withText?: boolean withText?: boolean
filledIcon?: boolean
} }
type BookingCodeChipWithoutCloseButtonProps = BaseBookingCodeChipProps & { type BookingCodeChipWithoutCloseButtonProps = BaseBookingCodeChipProps & {
withCloseButton?: false withCloseButton?: false
@@ -39,106 +37,68 @@ export function BookingCodeChip({
isCampaignUnavailable, isCampaignUnavailable,
isUnavailable, isUnavailable,
withText = true, withText = true,
filledIcon = false,
withCloseButton, withCloseButton,
onClose, onClose,
}: BookingCodeChipProps) { }: BookingCodeChipProps) {
const intl = useIntl() const intl = useIntl()
if (isCampaign || isCampaignUnavailable) { const isCampaignRate = isCampaign || isCampaignUnavailable
return ( if (!isCampaignRate && !bookingCode) {
<IconChip
color="green"
icon={
filledIcon ? (
<MaterialIcon
icon="sell"
color="Icon/Feedback/Success"
isFilled={!!filledIcon}
/>
) : (
<MaterialIcon icon="sell" color="Icon/Feedback/Success" />
)
}
className={alignCenter ? styles.center : undefined}
>
<p
className={cx(styles.bookingCodeChip, {
[styles.unavailable]: isCampaignUnavailable,
})}
>
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>
{intl.formatMessage({
id: 'booking.campaign',
defaultMessage: 'Campaign',
})}
</strong>
</Typography>
{bookingCode && (
<Typography variant="Body/Supporting text (caption)/smRegular">
{/*eslint-disable-next-line formatjs/no-literal-string-in-jsx*/}
<span> {bookingCode}</span>
</Typography>
)}
</p>
{withCloseButton && (
<IconButton
style="Muted"
theme="Inverted"
wrapping
className={styles.removeButton}
onPress={onClose}
aria-label={intl.formatMessage({
id: 'booking.removeBookingCode',
defaultMessage: 'Remove booking code',
})}
>
<MaterialIcon
icon="close"
size={16}
color="Icon/Feedback/Success"
/>
</IconButton>
)}
</IconChip>
)
}
if (!bookingCode) {
return null return null
} }
const color = isCampaignRate ? 'green' : 'blue'
const iconColor = isCampaignRate
? 'Icon/Feedback/Success'
: 'Icon/Feedback/Information'
const isUnavailableRate = isCampaignRate
? isCampaignUnavailable
: isUnavailable
const label = isCampaignRate
? intl.formatMessage({
id: 'booking.campaign',
defaultMessage: 'Campaign',
})
: intl.formatMessage({
id: 'booking.bookingCode',
defaultMessage: 'Booking code',
})
const icon = isCampaignRate ? (
<MaterialIcon icon="sell" color={iconColor} isFilled={false} size={20} />
) : (
<FilledDiscountIcon fill={iconColor} size={20} />
)
return ( return (
<IconChip <IconChip
color="blue" color={color}
icon={ icon={icon}
filledIcon ? (
<FilledDiscountIcon fill="Icon/Feedback/Information" />
) : (
<DiscountIcon color="Icon/Feedback/Information" />
)
}
className={alignCenter ? styles.center : undefined} className={alignCenter ? styles.center : undefined}
> >
<p <p
className={cx(styles.bookingCodeChip, { className={cx(styles.bookingCodeChip, {
[styles.unavailable]: isUnavailable, [styles.unavailable]: isUnavailableRate,
})} })}
> >
{withText && ( {withText && (
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<strong> <strong>{label}</strong>
{intl.formatMessage({ </Typography>
id: 'booking.bookingCode', )}
defaultMessage: 'Booking code',
})} {bookingCode && (
</strong> <Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{withText && <span className={styles.separator}></span>}
{bookingCode}
</span>
</Typography> </Typography>
)} )}
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>{bookingCode}</span>
</Typography>
</p> </p>
{withCloseButton && ( {withCloseButton && (
<IconButton <IconButton
@@ -152,11 +112,7 @@ export function BookingCodeChip({
defaultMessage: 'Remove booking code', defaultMessage: 'Remove booking code',
})} })}
> >
<MaterialIcon <MaterialIcon icon="close" size={16} color={iconColor} />
icon="close"
size={16}
color="Icon/Feedback/Information"
/>
</IconButton> </IconButton>
)} )}
</IconChip> </IconChip>