Merged in fix/STAY-133 (pull request #3313)
Fix/STAY-133 * fix: Add static summary buttons row on add ancillary flow * fix: refactor handling of modals * fix: refactor file structure for add ancillary flow * Merged in chore/replace-deprecated-body (pull request #3300) Replace deprecated <Body> with <Typography> * chore: replace deprecated body component * refactor: replace Body component with Typography across various components * merge Approved-by: Bianca Widstam Approved-by: Matilda Landström Approved-by: Bianca Widstam Approved-by: Matilda Landström
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.totalPointsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--Surface-Brand-Primary-2-OnSurface-Accent);
|
||||
padding: var(--Space-x1) var(--Space-x15);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
.guarantee {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
background-color: var(--Surface-Secondary-Default);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
padding: var(--Space-x2);
|
||||
}
|
||||
|
||||
.refundPolicy {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
.pointsAvailable {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.paymentInfo {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.totalPoints {
|
||||
display: flex;
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accordionItem {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
|
||||
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
|
||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||
|
||||
import styles from "./confirmationStep.module.css"
|
||||
|
||||
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function ConfirmationStep({
|
||||
savedCreditCards,
|
||||
user,
|
||||
error,
|
||||
}: ConfirmationStepProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { guaranteeInfo, selectedAncillary, booking } = useAddAncillaryStore(
|
||||
(state) => ({
|
||||
checkInDate: state.booking.checkInDate,
|
||||
guaranteeInfo: state.booking.guaranteeInfo,
|
||||
selectedAncillary: state.selectedAncillary,
|
||||
booking: state.booking,
|
||||
})
|
||||
)
|
||||
|
||||
const mustBeGuaranteed = !guaranteeInfo && booking.isGuaranteeable
|
||||
const quantityWithCard = useWatch({ name: "quantityWithCard" })
|
||||
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
|
||||
const currentPoints = user?.membership?.currentPoints ?? 0
|
||||
const totalPoints =
|
||||
quantityWithPoints && selectedAncillary?.points
|
||||
? selectedAncillary.points * quantityWithPoints
|
||||
: null
|
||||
|
||||
const accordionTitle = intl.formatMessage({
|
||||
id: "myStay.guarantee.guaranteeInformation",
|
||||
defaultMessage:
|
||||
"By adding your card, you also guarantee your room booking for late arrival ",
|
||||
})
|
||||
|
||||
const accordionContent = intl.formatMessage({
|
||||
id: "myStay.guarantee.guaranteeInformation.content",
|
||||
defaultMessage:
|
||||
"The hotel will hold your booking, even if you arrive after 18:00. Your card will only be charged in the event of a no-show.",
|
||||
})
|
||||
|
||||
if (!selectedAncillary) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
{error && <Alert type={error.type} text={error.message} />}
|
||||
{!!quantityWithPoints && (
|
||||
<>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.pointsToBeDeducted",
|
||||
defaultMessage: "Points to be deducted now",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<div className={styles.totalPointsContainer}>
|
||||
<div className={styles.totalPoints}>
|
||||
<MaterialIcon icon="diamond" />
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h2>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "common.numberOfPoints",
|
||||
defaultMessage:
|
||||
"{points, plural, one {# point} other {# points}}",
|
||||
},
|
||||
{ points: totalPoints }
|
||||
)}
|
||||
</h2>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.pointsAvailable}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "addAncillary.confirmationStep.pointsAvailable",
|
||||
defaultMessage: "{amount} points available",
|
||||
},
|
||||
{ amount: currentPoints }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!!quantityWithCard ? (
|
||||
<>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.reserveWithCard",
|
||||
defaultMessage: "Reserve with Card",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.refundPolicy}>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.refundPolicyNightBefore",
|
||||
defaultMessage:
|
||||
"All extras can be cancelled until 23:59 the night before arrival. Time selection and special requests can also be modified.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.guarantee}>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||
<span>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.headingText",
|
||||
defaultMessage: "Payment will be made on check-in",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.infoText",
|
||||
defaultMessage:
|
||||
"The card is used to reserve your extras. You will be charged in case of no-show.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
{guaranteeInfo ? (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "payment.savedCard",
|
||||
defaultMessage: "Saved card",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<PaymentOptionsGroup name="paymentMethod">
|
||||
<PaymentOption
|
||||
value={PaymentMethodEnum.card}
|
||||
type={PaymentMethodEnum.card}
|
||||
cardNumber={guaranteeInfo.maskedCard.slice(-4)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.card",
|
||||
defaultMessage: "Card",
|
||||
})}
|
||||
hideRadioButton
|
||||
/>
|
||||
</PaymentOptionsGroup>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon
|
||||
icon="credit_score"
|
||||
size={24}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.confirmationText",
|
||||
defaultMessage:
|
||||
"Confirm and provide your payment card details in the next step",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
{mustBeGuaranteed && (
|
||||
<AccordionItem
|
||||
title={accordionTitle}
|
||||
type="inline"
|
||||
className={styles.accordionItem}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{accordionContent}</p>
|
||||
</Typography>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{!!savedCreditCards?.length && <Divider />}
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName="paymentMethod"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.refundPolicy}>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.refundPolicyNightBefore",
|
||||
defaultMessage:
|
||||
"All extras can be cancelled until 23:59 the night before arrival. Time selection and special requests can also be modified.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
<TermsAndConditions />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.selectContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
padding: var(--Space-x3);
|
||||
margin-bottom: var(--Space-x05);
|
||||
background-color: var(--Background-Primary);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { generateDeliveryOptions } from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
|
||||
import styles from "./deliveryDetailsStep.module.css"
|
||||
|
||||
export default function DeliveryMethodStep() {
|
||||
const intl = useIntl()
|
||||
const deliveryTimeOptions = generateDeliveryOptions()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.selectContainer}>
|
||||
<div className={styles.select}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "ancillaries.deliveredAt",
|
||||
defaultMessage: "Delivered at:",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Select
|
||||
name="deliveryTime"
|
||||
label={""}
|
||||
items={deliveryTimeOptions}
|
||||
registerOptions={{ required: true }}
|
||||
isNestedInModal
|
||||
/>
|
||||
</div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.deliveryDetailsStep.deliveryTimeDescription",
|
||||
defaultMessage:
|
||||
"All extras are delivered together. Changes to delivery times will affect previously ordered extras.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.select}>
|
||||
<Input
|
||||
label={intl.formatMessage({
|
||||
id: "addAncillary.deliveryDetailsStep.optionalTextLabel",
|
||||
defaultMessage: "Other Requests",
|
||||
})}
|
||||
name="optionalText"
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "common.optional",
|
||||
defaultMessage: "Optional",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
AncillaryStepEnum,
|
||||
useAddAncillaryStore,
|
||||
} from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import ConfirmationStep from "../ConfirmationStep"
|
||||
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
||||
import SelectQuantityStep from "../SelectQuantityStep"
|
||||
|
||||
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function Desktop({ user, savedCreditCards, error }: StepsProps) {
|
||||
const currentStep = useAddAncillaryStore((state) => state.currentStep)
|
||||
|
||||
switch (currentStep) {
|
||||
case AncillaryStepEnum.selectQuantity:
|
||||
return <SelectQuantityStep user={user} />
|
||||
case AncillaryStepEnum.selectDelivery:
|
||||
return <DeliveryMethodStep />
|
||||
case AncillaryStepEnum.confirmation:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
savedCreditCards={savedCreditCards}
|
||||
user={user}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
AncillaryStepEnum,
|
||||
useAddAncillaryStore,
|
||||
} from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import ConfirmationStep from "../ConfirmationStep"
|
||||
import DeliveryMethodStep from "../DeliveryDetailsStep"
|
||||
import SelectQuantityStep from "../SelectQuantityStep"
|
||||
|
||||
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function Mobile({ user, savedCreditCards, error }: StepsProps) {
|
||||
const { currentStep, selectedAncillary } = useAddAncillaryStore((state) => ({
|
||||
currentStep: state.currentStep,
|
||||
selectedAncillary: state.selectedAncillary,
|
||||
}))
|
||||
|
||||
if (currentStep === AncillaryStepEnum.selectQuantity) {
|
||||
return (
|
||||
<>
|
||||
<SelectQuantityStep user={user} />
|
||||
{selectedAncillary?.requiresDeliveryTime && <DeliveryMethodStep />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ConfirmationStep
|
||||
savedCreditCards={savedCreditCards}
|
||||
user={user}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import styles from "./selectQuantityStep.module.css"
|
||||
|
||||
export function BreakfastInfo() {
|
||||
const intl = useIntl()
|
||||
const breakfastData = useAddAncillaryStore((state) => state.breakfastData)
|
||||
|
||||
if (!breakfastData) {
|
||||
return intl.formatMessage({
|
||||
id: "ancillaries.unableToDisplayBreakfastPrices",
|
||||
defaultMessage: "Unable to display breakfast prices.",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.breakfastContainer}>
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
text={intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.breakfastInfoMessage",
|
||||
defaultMessage:
|
||||
"Breakfast can only be added for the entire duration of the stay and for all guests.",
|
||||
})}
|
||||
/>
|
||||
{(breakfastData.nrOfPayingChildren > 0 ||
|
||||
breakfastData.nrOfFreeChildren > 0) && (
|
||||
<dl className={styles.breakfastPrices}>
|
||||
<div className={styles.breakfastPriceBox}>
|
||||
<MaterialIcon icon="check_circle" className={styles.icon} />
|
||||
<div>
|
||||
<dt>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${breakfastData.nrOfAdults} × ${intl.formatMessage({
|
||||
id: "common.adults",
|
||||
defaultMessage: "Adults",
|
||||
})}`}
|
||||
</dt>
|
||||
<dd>
|
||||
<Typography variant="Tag/sm">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<p>{`${breakfastData.priceAdult * breakfastData.nrOfAdults} ${breakfastData.currency}`}</p>
|
||||
</Typography>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{breakfastData.nrOfPayingChildren > 0 && (
|
||||
<div className={styles.breakfastPriceBox}>
|
||||
<MaterialIcon icon="check_circle" className={styles.icon} />
|
||||
<div>
|
||||
<dt>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${breakfastData.nrOfPayingChildren} × ${intl.formatMessage({
|
||||
id: "common.ages",
|
||||
defaultMessage: "ages",
|
||||
})} 4-12`}
|
||||
</dt>
|
||||
<dd>
|
||||
<Typography variant="Tag/sm">
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
<p>{`${breakfastData.priceChild * breakfastData.nrOfPayingChildren} ${breakfastData.currency}`}</p>
|
||||
</Typography>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{breakfastData.nrOfFreeChildren > 0 && (
|
||||
<div className={`${styles.breakfastPriceBox} ${styles.free}`}>
|
||||
<MaterialIcon icon="check_circle" className={styles.icon} />
|
||||
<div>
|
||||
<dt>
|
||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||
{`${breakfastData.nrOfFreeChildren} × ${intl.formatMessage({
|
||||
defaultMessage: "under",
|
||||
id: "common.under",
|
||||
})} 4`}
|
||||
</dt>
|
||||
<dd>
|
||||
<Typography variant="Tag/sm">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Free",
|
||||
id: "common.free",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { type ReactNode } from "react"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||
import { getErrorMessage } from "@/utils/getErrorMessage"
|
||||
|
||||
import { BreakfastInfo } from "./BreakfastInfo"
|
||||
|
||||
import styles from "./selectQuantityStep.module.css"
|
||||
|
||||
import type {
|
||||
InnerSelectQuantityStepProps,
|
||||
SelectQuantityStepProps,
|
||||
} from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
const cardQuantityOptions = Array.from({ length: 7 }, (_, i) => ({
|
||||
label: `${i}`,
|
||||
value: i,
|
||||
}))
|
||||
|
||||
export default function SelectQuantityStep({ user }: SelectQuantityStepProps) {
|
||||
const { isBreakfast, selectedAncillary } = useAddAncillaryStore((state) => ({
|
||||
isBreakfast: state.isBreakfast,
|
||||
selectedAncillary: state.selectedAncillary,
|
||||
}))
|
||||
|
||||
let content: ReactNode
|
||||
|
||||
if (isBreakfast) {
|
||||
content = <BreakfastInfo />
|
||||
} else if (!selectedAncillary?.requiresQuantity) {
|
||||
content = null
|
||||
} else {
|
||||
content = (
|
||||
<InnerSelectQuantityStep
|
||||
user={user}
|
||||
selectedAncillary={selectedAncillary}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className={styles.container}>{content}</div>
|
||||
}
|
||||
|
||||
function InnerSelectQuantityStep({
|
||||
user,
|
||||
selectedAncillary,
|
||||
}: InnerSelectQuantityStepProps) {
|
||||
const intl = useIntl()
|
||||
const {
|
||||
formState: { errors },
|
||||
} = useFormContext()
|
||||
|
||||
const pointsCost = selectedAncillary?.points ?? 0
|
||||
const currentPoints = user?.membership?.currentPoints ?? 0
|
||||
const maxAffordable =
|
||||
pointsCost > 0 ? Math.min(Math.floor(currentPoints / pointsCost), 7) : 0
|
||||
|
||||
const pointsQuantityOptions = Array.from(
|
||||
{ length: maxAffordable + 1 },
|
||||
(_, i) => ({
|
||||
label: `${i}`,
|
||||
value: i,
|
||||
})
|
||||
)
|
||||
|
||||
const insufficientPoints = currentPoints < pointsCost || currentPoints === 0
|
||||
|
||||
return (
|
||||
<div className={styles.selectContainer}>
|
||||
{selectedAncillary?.points && user && (
|
||||
<div className={styles.select}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2 className={styles.selectTitle}>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.payWithPoints",
|
||||
defaultMessage: "Pay with points",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<div className={styles.totalPointsContainer}>
|
||||
<div className={styles.totalPoints}>
|
||||
<MaterialIcon icon="diamond" />
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h2>
|
||||
{intl.formatMessage({
|
||||
id: "common.totalPoints",
|
||||
defaultMessage: "Total points",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{currentPoints}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
{insufficientPoints ? (
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<h2 className={styles.insufficientPoints}>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.insufficientPoints",
|
||||
defaultMessage: "Insufficient points",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
) : (
|
||||
<Select
|
||||
name="quantityWithPoints"
|
||||
label={intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.selectQuantityLabel",
|
||||
defaultMessage: "Select quantity",
|
||||
})}
|
||||
items={pointsQuantityOptions}
|
||||
isNestedInModal
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.select}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2 className={styles.selectTitle}>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.payWithCard",
|
||||
defaultMessage: "Pay with card at the hotel",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<Select
|
||||
name="quantityWithCard"
|
||||
label={intl.formatMessage({
|
||||
id: "addAncillary.selectQuantityStep.selectQuantityLabel",
|
||||
defaultMessage: "Select quantity",
|
||||
})}
|
||||
items={cardQuantityOptions}
|
||||
isNestedInModal
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name="quantityWithCard"
|
||||
messageLabel={getErrorMessage(
|
||||
intl,
|
||||
errors["quantityWithCard"]?.message?.toString()
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.selectContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x025);
|
||||
}
|
||||
|
||||
.select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
background-color: var(--Surface-Primary-OnSurface-Default);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
margin-bottom: var(--Space-x1);
|
||||
}
|
||||
|
||||
.selectTitle {
|
||||
margin-bottom: var(--Space-x1);
|
||||
}
|
||||
|
||||
.insufficientPoints {
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
|
||||
.totalPointsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--Surface-Brand-Primary-2-OnSurface-Accent);
|
||||
padding: var(--Space-x1) var(--Space-x15);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
|
||||
.totalPoints {
|
||||
display: flex;
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breakfastContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.breakfastPrices {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.breakfastPriceBox {
|
||||
display: flex;
|
||||
padding: var(--Space-x15);
|
||||
flex: 1 0 0;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
background: var(--Surface-Feedback-Information-light);
|
||||
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.free {
|
||||
background: var(--Surface-Feedback-Succes-light);
|
||||
}
|
||||
|
||||
.breakfastPriceBox dt {
|
||||
color: var(--Text-Secondary);
|
||||
|
||||
font-family: var(--Tag-Font-family);
|
||||
font-size: var(--Tag-Size);
|
||||
font-style: normal;
|
||||
font-weight: var(--Tag-Font-weight);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import Desktop from "./Desktop"
|
||||
import Mobile from "./Mobile"
|
||||
|
||||
import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
|
||||
export default function Steps(props: StepsProps) {
|
||||
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||
|
||||
return isMobile ? <Mobile {...props} /> : <Desktop {...props} />
|
||||
}
|
||||
Reference in New Issue
Block a user