Merged in feat/BOOK-426-add-campaign-tag-select-hotel (pull request #3023)
Feat/BOOK-426 add campaign tag select hotel * feat(BOOK-426): introduce campaign tag on select hotel card * feat(BOOK-426): remove redundant tags * feat(BOOK-426): fix comments, change to typography * feat(BOOK-426): fix comments, update to cx Approved-by: Erik Tiekstra Approved-by: Matilda Landström
This commit is contained in:
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation"
|
|||||||
import { useEffect, useMemo, useRef } from "react"
|
import { useEffect, useMemo, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||||
import {
|
import {
|
||||||
alternativeHotelsMap,
|
alternativeHotelsMap,
|
||||||
selectHotelMap,
|
selectHotelMap,
|
||||||
@@ -82,6 +83,18 @@ export default function HotelCardListing({
|
|||||||
isBookingCodeRateAvailable &&
|
isBookingCodeRateAvailable &&
|
||||||
activeCodeFilter === BookingCodeFilterEnum.Discounted
|
activeCodeFilter === BookingCodeFilterEnum.Discounted
|
||||||
|
|
||||||
|
const isCampaignWithBookingCode =
|
||||||
|
!!bookingCode &&
|
||||||
|
hotelData
|
||||||
|
.filter((hotel) => hotel.availability.bookingCode)
|
||||||
|
.every(
|
||||||
|
(hotel) =>
|
||||||
|
hotel.availability.productType?.public?.rateType ===
|
||||||
|
RateTypeEnum.PublicPromotion ||
|
||||||
|
hotel.availability.productType?.member?.rateType ===
|
||||||
|
RateTypeEnum.PublicPromotion
|
||||||
|
)
|
||||||
|
|
||||||
const unfilteredHotelCount = showOnlyBookingCodeRates
|
const unfilteredHotelCount = showOnlyBookingCodeRates
|
||||||
? hotelData.filter((hotel) => hotel.availability.bookingCode).length
|
? hotelData.filter((hotel) => hotel.availability.bookingCode).length
|
||||||
: hotelData.length
|
: hotelData.length
|
||||||
@@ -233,6 +246,7 @@ export default function HotelCardListing({
|
|||||||
}
|
}
|
||||||
type={type}
|
type={type}
|
||||||
bookingCode={bookingCode}
|
bookingCode={bookingCode}
|
||||||
|
isCampaignWithBookingCode={isCampaignWithBookingCode}
|
||||||
isAlternative={isAlternative}
|
isAlternative={isAlternative}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MaterialIcon } from '../Icons/MaterialIcon'
|
|||||||
import { Typography } from '../Typography'
|
import { Typography } from '../Typography'
|
||||||
|
|
||||||
import styles from './bookingCodeChip.module.css'
|
import styles from './bookingCodeChip.module.css'
|
||||||
|
import { cx } from 'class-variance-authority'
|
||||||
import { IconButton } from '../IconButton'
|
import { IconButton } from '../IconButton'
|
||||||
|
|
||||||
type BaseBookingCodeChipProps = {
|
type BaseBookingCodeChipProps = {
|
||||||
@@ -14,6 +15,7 @@ type BaseBookingCodeChipProps = {
|
|||||||
bookingCode?: string | null
|
bookingCode?: string | null
|
||||||
isCampaign?: boolean
|
isCampaign?: boolean
|
||||||
isUnavailable?: boolean
|
isUnavailable?: boolean
|
||||||
|
isCampaignUnavailable?: boolean
|
||||||
withText?: boolean
|
withText?: boolean
|
||||||
filledIcon?: boolean
|
filledIcon?: boolean
|
||||||
}
|
}
|
||||||
@@ -34,6 +36,7 @@ export function BookingCodeChip({
|
|||||||
alignCenter,
|
alignCenter,
|
||||||
bookingCode,
|
bookingCode,
|
||||||
isCampaign,
|
isCampaign,
|
||||||
|
isCampaignUnavailable,
|
||||||
isUnavailable,
|
isUnavailable,
|
||||||
withText = true,
|
withText = true,
|
||||||
filledIcon = false,
|
filledIcon = false,
|
||||||
@@ -42,7 +45,7 @@ export function BookingCodeChip({
|
|||||||
}: BookingCodeChipProps) {
|
}: BookingCodeChipProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
if (isCampaign) {
|
if (isCampaign || isCampaignUnavailable) {
|
||||||
return (
|
return (
|
||||||
<IconChip
|
<IconChip
|
||||||
color="green"
|
color="green"
|
||||||
@@ -59,7 +62,11 @@ export function BookingCodeChip({
|
|||||||
}
|
}
|
||||||
className={alignCenter ? styles.center : undefined}
|
className={alignCenter ? styles.center : undefined}
|
||||||
>
|
>
|
||||||
<p className={styles.bookingCodeChip}>
|
<p
|
||||||
|
className={cx(styles.bookingCodeChip, {
|
||||||
|
[styles.unavailable]: isCampaignUnavailable,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
<strong>
|
<strong>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -115,7 +122,9 @@ export function BookingCodeChip({
|
|||||||
className={alignCenter ? styles.center : undefined}
|
className={alignCenter ? styles.center : undefined}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ''}`}
|
className={cx(styles.bookingCodeChip, {
|
||||||
|
[styles.unavailable]: isUnavailable,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{withText && (
|
{withText && (
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
|||||||
@@ -16,6 +16,16 @@
|
|||||||
padding: var(--Spacing-x-quarter) 0;
|
padding: var(--Spacing-x-quarter) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.redColor {
|
||||||
|
color: var(--Text-Accent-Primary);
|
||||||
|
}
|
||||||
|
.defaultColor {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
.secondaryColor {
|
||||||
|
color: var(--Text-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { cx } from 'class-variance-authority'
|
import { cx } from 'class-variance-authority'
|
||||||
import { useIntl } from 'react-intl'
|
import { useIntl } from 'react-intl'
|
||||||
|
|
||||||
import Body from '../../Body'
|
|
||||||
import Caption from '../../Caption'
|
|
||||||
import { Divider } from '../../Divider'
|
import { Divider } from '../../Divider'
|
||||||
import Subtitle from '../../Subtitle'
|
|
||||||
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
|
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
|
||||||
|
import { Typography } from '../../Typography'
|
||||||
import styles from './hotelPriceCard.module.css'
|
import styles from './hotelPriceCard.module.css'
|
||||||
|
|
||||||
type Price = {
|
type Price = {
|
||||||
@@ -23,12 +20,14 @@ export type PriceCardProps = {
|
|||||||
}
|
}
|
||||||
isMemberPrice?: boolean
|
isMemberPrice?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
isCampaign?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HotelPriceCard({
|
export function HotelPriceCard({
|
||||||
productTypePrices,
|
productTypePrices,
|
||||||
isMemberPrice = false,
|
isMemberPrice = false,
|
||||||
className,
|
className,
|
||||||
|
isCampaign = false,
|
||||||
}: PriceCardProps) {
|
}: PriceCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const isRegularOrPublicPromotionRate =
|
const isRegularOrPublicPromotionRate =
|
||||||
@@ -41,79 +40,111 @@ export function HotelPriceCard({
|
|||||||
(isMemberPrice ? (
|
(isMemberPrice ? (
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption color="red">
|
<Typography
|
||||||
{intl.formatMessage({
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
id: 'booking.memberPrice',
|
className={styles.redColor}
|
||||||
defaultMessage: 'Member price',
|
>
|
||||||
})}
|
<p>
|
||||||
</Caption>
|
{intl.formatMessage({
|
||||||
|
id: 'booking.memberPrice',
|
||||||
|
defaultMessage: 'Member price',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dt>
|
</dt>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextHighContrast">
|
<Typography
|
||||||
{intl.formatMessage({
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
id: 'booking.standardPrice',
|
className={styles.defaultColor}
|
||||||
defaultMessage: 'Standard price',
|
>
|
||||||
})}
|
<p>
|
||||||
</Caption>
|
{intl.formatMessage({
|
||||||
|
id: 'booking.standardPrice',
|
||||||
|
defaultMessage: 'Standard price',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dt>
|
</dt>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption
|
<Typography
|
||||||
type="bold"
|
variant="Body/Supporting text (caption)/smBold"
|
||||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
className={isMemberPrice ? styles.redColor : styles.defaultColor}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
<p>
|
||||||
id: 'common.from',
|
{intl.formatMessage({
|
||||||
defaultMessage: 'From',
|
id: 'common.from',
|
||||||
})}
|
defaultMessage: 'From',
|
||||||
</Caption>
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<div className={styles.price}>
|
<div className={styles.price}>
|
||||||
<Subtitle
|
<Typography
|
||||||
type="two"
|
variant="Title/Subtitle/md"
|
||||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
className={
|
||||||
|
isMemberPrice || isCampaign
|
||||||
|
? styles.redColor
|
||||||
|
: styles.defaultColor
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{productTypePrices.localPrice.pricePerNight}
|
<p> {productTypePrices.localPrice.pricePerNight}</p>
|
||||||
</Subtitle>
|
</Typography>
|
||||||
<Body
|
<Typography
|
||||||
color={isMemberPrice ? 'red' : 'uiTextHighContrast'}
|
variant="Body/Paragraph/mdBold"
|
||||||
textTransform="bold"
|
className={
|
||||||
|
isMemberPrice || isCampaign
|
||||||
|
? styles.redColor
|
||||||
|
: styles.defaultColor
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{productTypePrices.localPrice.currency}
|
<p>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{productTypePrices.localPrice.currency}
|
||||||
<span className={styles.perNight}>
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
/
|
<span className={styles.perNight}>
|
||||||
{intl.formatMessage({
|
/
|
||||||
id: 'common.night',
|
{intl.formatMessage({
|
||||||
defaultMessage: 'night',
|
id: 'common.night',
|
||||||
})}
|
defaultMessage: 'night',
|
||||||
</span>
|
})}
|
||||||
</Body>
|
</span>
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{productTypePrices?.requestedPrice && (
|
{productTypePrices?.requestedPrice && (
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Typography
|
||||||
{intl.formatMessage({
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
id: 'booking.approx',
|
className={styles.secondaryColor}
|
||||||
defaultMessage: 'Approx.',
|
>
|
||||||
})}
|
<p>
|
||||||
</Caption>
|
{intl.formatMessage({
|
||||||
|
id: 'booking.approx',
|
||||||
|
defaultMessage: 'Approx.',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Caption color={'uiTextMediumContrast'}>
|
<Typography
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
{`${productTypePrices.requestedPrice.pricePerNight} `}
|
className={styles.secondaryColor}
|
||||||
{productTypePrices.requestedPrice.currency}
|
>
|
||||||
</Caption>
|
<p>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{`${productTypePrices.requestedPrice.pricePerNight} `}
|
||||||
|
{productTypePrices.requestedPrice.currency}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -125,19 +156,29 @@ export function HotelPriceCard({
|
|||||||
<Divider className={styles.divider} />
|
<Divider className={styles.divider} />
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Typography
|
||||||
{intl.formatMessage({
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
id: 'common.total',
|
className={styles.secondaryColor}
|
||||||
defaultMessage: 'Total',
|
>
|
||||||
})}
|
<p>
|
||||||
</Caption>
|
{intl.formatMessage({
|
||||||
|
id: 'common.total',
|
||||||
|
defaultMessage: 'Total',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<Caption color={'uiTextMediumContrast'}>
|
<Typography
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
{`${productTypePrices.localPrice.pricePerStay} `}
|
className={styles.secondaryColor}
|
||||||
{productTypePrices.localPrice.currency}
|
>
|
||||||
</Caption>
|
<p>
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
{`${productTypePrices.localPrice.pricePerStay} `}
|
||||||
|
{productTypePrices.localPrice.currency}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export type HotelCardProps = {
|
|||||||
isAlternative?: boolean
|
isAlternative?: boolean
|
||||||
pointsCurrency?: CurrencyEnum
|
pointsCurrency?: CurrencyEnum
|
||||||
fullPrice: boolean
|
fullPrice: boolean
|
||||||
|
isCampaignWithBookingCode: boolean
|
||||||
lang: Lang
|
lang: Lang
|
||||||
|
|
||||||
belowInfoSlot: React.ReactNode
|
belowInfoSlot: React.ReactNode
|
||||||
@@ -133,6 +133,7 @@ export const HotelCard = memo(
|
|||||||
lang,
|
lang,
|
||||||
belowInfoSlot,
|
belowInfoSlot,
|
||||||
fullPrice,
|
fullPrice,
|
||||||
|
isCampaignWithBookingCode,
|
||||||
onAddressClick,
|
onAddressClick,
|
||||||
onHover,
|
onHover,
|
||||||
onHoverEnd,
|
onHoverEnd,
|
||||||
@@ -166,6 +167,10 @@ export const HotelCard = memo(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isDisabled = prices?.redemptions?.length && hasInsufficientPoints
|
const isDisabled = prices?.redemptions?.length && hasInsufficientPoints
|
||||||
|
const isCampaign =
|
||||||
|
prices?.public?.rateType === RateTypeEnum.PublicPromotion ||
|
||||||
|
prices?.member?.rateType === RateTypeEnum.PublicPromotion
|
||||||
|
const showBookingCodeChip = bookingCode || isCampaign
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
@@ -267,10 +272,14 @@ export const HotelCard = memo(
|
|||||||
<NoPriceAvailableCard />
|
<NoPriceAvailableCard />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bookingCode && (
|
{showBookingCodeChip && (
|
||||||
<BookingCodeChip
|
<BookingCodeChip
|
||||||
bookingCode={bookingCode}
|
bookingCode={bookingCode}
|
||||||
isUnavailable={fullPrice}
|
isUnavailable={fullPrice}
|
||||||
|
isCampaignUnavailable={
|
||||||
|
isCampaignWithBookingCode && fullPrice
|
||||||
|
}
|
||||||
|
isCampaign={isCampaign}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(!isUserLoggedIn ||
|
{(!isUserLoggedIn ||
|
||||||
@@ -280,6 +289,7 @@ export const HotelCard = memo(
|
|||||||
<HotelPriceCard
|
<HotelPriceCard
|
||||||
productTypePrices={prices.public}
|
productTypePrices={prices.public}
|
||||||
className={styles.priceCard}
|
className={styles.priceCard}
|
||||||
|
isCampaign={isCampaign}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{prices.member && (
|
{prices.member && (
|
||||||
|
|||||||
Reference in New Issue
Block a user