feat: SW-1583 Enabled reward night in booking widget
This commit is contained in:
@@ -102,8 +102,7 @@ export default function BookingWidgetClient({
|
|||||||
value: selectedBookingCode,
|
value: selectedBookingCode,
|
||||||
remember: false,
|
remember: false,
|
||||||
},
|
},
|
||||||
redemption: false,
|
redemption: bookingWidgetSearchData?.searchType === "redemption",
|
||||||
voucher: false,
|
|
||||||
rooms: defaultRoomsData,
|
rooms: defaultRoomsData,
|
||||||
},
|
},
|
||||||
shouldFocusError: false,
|
shouldFocusError: false,
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { ErrorCircleIcon } from "@/components/Icons"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./reward-night.module.css"
|
||||||
|
|
||||||
|
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||||
|
|
||||||
|
export function RewardNight() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const {
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
|
formState: { errors },
|
||||||
|
trigger,
|
||||||
|
} = useFormContext<BookingWidgetSchema>()
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
const reward = intl.formatMessage({ id: "Book Reward Night" })
|
||||||
|
const redemptionErr = errors["redemption"]
|
||||||
|
const bookingCode = getValues("bookingCode.value")
|
||||||
|
const isMultiRoomError = redemptionErr?.message?.indexOf("Multi-room") === 0
|
||||||
|
const errorInfoColor = isMultiRoomError ? "red" : "blue"
|
||||||
|
|
||||||
|
function validateBookingWidget(value: boolean) {
|
||||||
|
trigger("redemption")
|
||||||
|
if (value && bookingCode) {
|
||||||
|
setValue("bookingCode.value", "", { shouldValidate: true })
|
||||||
|
setTimeout(() => {
|
||||||
|
trigger("redemption")
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetOnMultiroomError = useCallback(() => {
|
||||||
|
if (isMultiRoomError) {
|
||||||
|
setValue("redemption", false, { shouldValidate: true })
|
||||||
|
}
|
||||||
|
}, [isMultiRoomError, setValue])
|
||||||
|
|
||||||
|
function closeOnBlur(evt: FocusEvent) {
|
||||||
|
const target = evt.relatedTarget as HTMLElement
|
||||||
|
if (ref.current && target && !ref.current.contains(target)) {
|
||||||
|
resetOnMultiroomError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const clearIfOutside = function (evt: Event) {
|
||||||
|
const target = evt.target as HTMLElement
|
||||||
|
if (ref.current && target && !ref.current.contains(target)) {
|
||||||
|
resetOnMultiroomError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.addEventListener("click", clearIfOutside)
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener("click", clearIfOutside)
|
||||||
|
}
|
||||||
|
}, [resetOnMultiroomError, ref])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} onBlur={(e) => closeOnBlur(e.nativeEvent)}>
|
||||||
|
<Checkbox
|
||||||
|
hideError
|
||||||
|
name="redemption"
|
||||||
|
registerOptions={{
|
||||||
|
onChange: (e) => {
|
||||||
|
validateBookingWidget(e.target.value)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Caption color="uiTextMediumContrast" asChild>
|
||||||
|
<span>{reward}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
{redemptionErr && (
|
||||||
|
<div className={styles.errorContainer}>
|
||||||
|
<Caption
|
||||||
|
className={styles.error}
|
||||||
|
type="regular"
|
||||||
|
color={errorInfoColor}
|
||||||
|
>
|
||||||
|
<ErrorCircleIcon
|
||||||
|
color={errorInfoColor}
|
||||||
|
className={styles.errorIcon}
|
||||||
|
/>
|
||||||
|
{intl.formatMessage({ id: redemptionErr.message })}
|
||||||
|
</Caption>
|
||||||
|
{isMultiRoomError ? (
|
||||||
|
// ToDo: Replace with Remove extra rooms JSX element after SW-963 is merged
|
||||||
|
<Body>{"Remove extra rooms"}</Body>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.errorContainer {
|
||||||
|
background: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border-radius: var(--Spacing-x-one-and-half);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 16px);
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIcon {
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@
|
|||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { env } from "@/env/client"
|
||||||
|
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
||||||
|
|
||||||
import BookingCode from "../BookingCode"
|
import BookingCode from "../BookingCode"
|
||||||
|
import { RewardNight } from "../RewardNight"
|
||||||
|
|
||||||
import styles from "./voucher.module.css"
|
import styles from "./voucher.module.css"
|
||||||
|
|
||||||
@@ -31,36 +34,44 @@ export default function Voucher() {
|
|||||||
<BookingCode />
|
<BookingCode />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
<div className={styles.option}>
|
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
|
||||||
<Tooltip
|
<>
|
||||||
heading={disabledBookingOptionsHeader}
|
<div className={styles.option}>
|
||||||
text={disabledBookingOptionsText}
|
<Tooltip
|
||||||
position="bottom"
|
heading={disabledBookingOptionsHeader}
|
||||||
arrow="left"
|
text={disabledBookingOptionsText}
|
||||||
>
|
position="bottom"
|
||||||
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
arrow="left"
|
||||||
<Caption color="disabled" asChild>
|
>
|
||||||
<span>{bonus}</span>
|
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
||||||
</Caption>
|
<Caption color="disabled" asChild>
|
||||||
</Checkbox>
|
<span>{bonus}</span>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
</Caption>
|
||||||
</Tooltip>
|
</Checkbox>
|
||||||
</div>
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
<div className={styles.option}>
|
</Tooltip>
|
||||||
<Tooltip
|
</div>
|
||||||
heading={disabledBookingOptionsHeader}
|
<div className={styles.option}>
|
||||||
text={disabledBookingOptionsText}
|
<Tooltip
|
||||||
position="bottom"
|
heading={disabledBookingOptionsHeader}
|
||||||
arrow="left"
|
text={disabledBookingOptionsText}
|
||||||
>
|
position="bottom"
|
||||||
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
|
arrow="left"
|
||||||
<Caption color="disabled" asChild>
|
>
|
||||||
<span>{reward}</span>
|
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
|
||||||
</Caption>
|
<Caption color="disabled" asChild>
|
||||||
</Checkbox>
|
<span>{reward}</span>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
</Caption>
|
||||||
</Tooltip>
|
</Checkbox>
|
||||||
</div>
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.option}>
|
||||||
|
<RewardNight />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -87,16 +98,18 @@ export function VoucherSkeleton() {
|
|||||||
<SkeletonShimmer width={"100%"} />
|
<SkeletonShimmer width={"100%"} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
|
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? null : (
|
||||||
|
<div className={styles.option}>
|
||||||
|
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
||||||
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{bonus}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.option}>
|
<div className={styles.option}>
|
||||||
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
<Checkbox name="redemption">
|
||||||
<Caption color="disabled" asChild>
|
<Caption color="uiTextMediumContrast" asChild>
|
||||||
<span>{bonus}</span>
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div className={styles.option}>
|
|
||||||
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
|
|
||||||
<Caption color="disabled" asChild>
|
|
||||||
<span>{reward}</span>
|
<span>{reward}</span>
|
||||||
</Caption>
|
</Caption>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export default function Form({
|
|||||||
...(data.bookingCode?.value
|
...(data.bookingCode?.value
|
||||||
? { bookingCode: data.bookingCode.value }
|
? { bookingCode: data.bookingCode.value }
|
||||||
: {}),
|
: {}),
|
||||||
|
// Followed current url structure to keep searchType=redemption param incase of reward night
|
||||||
|
...(data.redemption ? { searchType: "redemption" } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
onClose()
|
onClose()
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export const bookingWidgetSchema = z
|
|||||||
redemption: z.boolean().default(false),
|
redemption: z.boolean().default(false),
|
||||||
rooms: guestRoomsSchema,
|
rooms: guestRoomsSchema,
|
||||||
search: z.string({ coerce: true }).min(1, "Required"),
|
search: z.string({ coerce: true }).min(1, "Required"),
|
||||||
voucher: z.boolean().default(false),
|
|
||||||
hotel: z.number().optional(),
|
hotel: z.number().optional(),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
})
|
})
|
||||||
@@ -130,4 +129,28 @@ export const bookingWidgetSchema = z
|
|||||||
path: ["rooms"],
|
path: ["rooms"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (value.rooms.length > 1 && value.redemption) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Multi-room booking is not available with reward night.",
|
||||||
|
path: ["redemption"],
|
||||||
|
})
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Multi-room booking is not available with reward night.",
|
||||||
|
path: ["rooms"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (value.bookingCode?.value && value.redemption) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Code and voucher is not available with reward night.",
|
||||||
|
path: ["redemption"],
|
||||||
|
})
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Code and voucher is not available with reward night.",
|
||||||
|
path: ["bookingCode.value"],
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,9 +37,8 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
|
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
|
||||||
const addRoomLabel = intl.formatMessage({ id: "Add room" })
|
const addRoomLabel = intl.formatMessage({ id: "Add room" })
|
||||||
const doneLabel = intl.formatMessage({ id: "Done" })
|
const doneLabel = intl.formatMessage({ id: "Done" })
|
||||||
|
// Disable add room if booking code is either voucher or corporate cheque, or reward night is enabled
|
||||||
// Disable add room if booking code is voucher or reward night is enebaled
|
const addRoomDisabledTextForSpecialRate = getValues("redemption")
|
||||||
const addRoomDisabledText = getValues("redemption")
|
|
||||||
? intl.formatMessage({
|
? intl.formatMessage({
|
||||||
id: "Multi-room booking is not available with reward night.",
|
id: "Multi-room booking is not available with reward night.",
|
||||||
})
|
})
|
||||||
@@ -102,39 +101,52 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{canAddRooms && (
|
{addRoomDisabledTextForSpecialRate ? (
|
||||||
<div className={styles.addRoomMobileContainer}>
|
<div className={styles.addRoomMobileContainer}>
|
||||||
<Button
|
<Tooltip
|
||||||
intent="text"
|
text={addRoomDisabledTextForSpecialRate}
|
||||||
variant="icon"
|
position="bottom"
|
||||||
wrapping
|
arrow="left"
|
||||||
theme="base"
|
|
||||||
fullWidth
|
|
||||||
onPress={handleAddRoom}
|
|
||||||
disabled={addRoomDisabledText ? true : false}
|
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<Button
|
||||||
{addRoomLabel}
|
intent="text"
|
||||||
</Button>
|
variant="icon"
|
||||||
{addRoomDisabledText ? (
|
wrapping
|
||||||
<Caption
|
theme="base"
|
||||||
color="blue"
|
fullWidth
|
||||||
className={styles.addRoomMobileDisabledText}
|
onPress={handleAddRoom}
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<ErrorCircleIcon color="blue" width={20} />
|
<PlusIcon />
|
||||||
{addRoomDisabledText}
|
{addRoomLabel}
|
||||||
</Caption>
|
</Button>
|
||||||
) : null}
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
canAddRooms && (
|
||||||
|
<div className={styles.addRoomMobileContainer}>
|
||||||
|
<Button
|
||||||
|
intent="text"
|
||||||
|
variant="icon"
|
||||||
|
wrapping
|
||||||
|
theme="base"
|
||||||
|
fullWidth
|
||||||
|
onPress={handleAddRoom}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
{addRoomLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{addRoomDisabledText ? (
|
{addRoomDisabledTextForSpecialRate ? (
|
||||||
<div className={styles.hideOnMobile}>
|
<div className={styles.hideOnMobile}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
text={addRoomDisabledText}
|
text={addRoomDisabledTextForSpecialRate}
|
||||||
position={"bottom"}
|
position="bottom"
|
||||||
arrow="left"
|
arrow="left"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -143,6 +155,7 @@ export default function GuestsRoomsPickerDialog({
|
|||||||
wrapping
|
wrapping
|
||||||
theme="base"
|
theme="base"
|
||||||
disabled
|
disabled
|
||||||
|
onPress={handleAddRoom}
|
||||||
>
|
>
|
||||||
<PlusCircleIcon />
|
<PlusCircleIcon />
|
||||||
{addRoomLabel}
|
{addRoomLabel}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default function Checkbox({
|
|||||||
name,
|
name,
|
||||||
children,
|
children,
|
||||||
registerOptions,
|
registerOptions,
|
||||||
|
hideError,
|
||||||
}: React.PropsWithChildren<CheckboxProps>) {
|
}: React.PropsWithChildren<CheckboxProps>) {
|
||||||
const { control } = useFormContext()
|
const { control } = useFormContext()
|
||||||
const { field, fieldState } = useController({
|
const { field, fieldState } = useController({
|
||||||
@@ -44,7 +45,7 @@ export default function Checkbox({
|
|||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
{fieldState.error ? (
|
{fieldState.error && !hideError ? (
|
||||||
<Caption className={styles.error} fontOnly>
|
<Caption className={styles.error} fontOnly>
|
||||||
<InfoCircleIcon color="red" />
|
<InfoCircleIcon color="red" />
|
||||||
{fieldState.error.message}
|
{fieldState.error.message}
|
||||||
|
|||||||
@@ -150,6 +150,7 @@
|
|||||||
"Close the map": "Close the map",
|
"Close the map": "Close the map",
|
||||||
"Closed": "Closed",
|
"Closed": "Closed",
|
||||||
"Code / Voucher": "Code / Voucher",
|
"Code / Voucher": "Code / Voucher",
|
||||||
|
"Code and voucher is not available with reward night.": "Code and voucher is not available with reward night.",
|
||||||
"Codes, cheques and reward nights aren't available on the new website yet.": "Codes, cheques and reward nights aren't available on the new website yet.",
|
"Codes, cheques and reward nights aren't available on the new website yet.": "Codes, cheques and reward nights aren't available on the new website yet.",
|
||||||
"Coming up": "Coming up",
|
"Coming up": "Coming up",
|
||||||
"Compare all levels": "Compare all levels",
|
"Compare all levels": "Compare all levels",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type BookingWidgetSearchData = {
|
|||||||
toDate?: string
|
toDate?: string
|
||||||
rooms?: TGuestsRoom[]
|
rooms?: TGuestsRoom[]
|
||||||
bookingCode?: string
|
bookingCode?: string
|
||||||
|
searchType?: "redemption"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingWidgetType = VariantProps<
|
export type BookingWidgetType = VariantProps<
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export interface CheckboxProps
|
|||||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
name: string
|
name: string
|
||||||
registerOptions?: RegisterOptions
|
registerOptions?: RegisterOptions
|
||||||
|
hideError?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const keyedSearchParams = new Map([
|
|||||||
["todate", "toDate"],
|
["todate", "toDate"],
|
||||||
["hotel", "hotelId"],
|
["hotel", "hotelId"],
|
||||||
["child", "childrenInRoom"],
|
["child", "childrenInRoom"],
|
||||||
|
["searchtype", "searchType"],
|
||||||
])
|
])
|
||||||
|
|
||||||
export type SelectHotelParams<T> = Omit<T, "hotel"> & {
|
export type SelectHotelParams<T> = Omit<T, "hotel"> & {
|
||||||
|
|||||||
Reference in New Issue
Block a user