feat: SW-1583 Enabled reward night in booking widget

This commit is contained in:
Hrishikesh Vaipurkar
2025-02-27 11:41:04 +01:00
parent 6232ded991
commit 51b70f3032
12 changed files with 247 additions and 69 deletions

View File

@@ -102,8 +102,7 @@ export default function BookingWidgetClient({
value: selectedBookingCode,
remember: false,
},
redemption: false,
voucher: false,
redemption: bookingWidgetSearchData?.searchType === "redemption",
rooms: defaultRoomsData,
},
shouldFocusError: false,

View File

@@ -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>
)
}

View File

@@ -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;
}

View File

@@ -2,12 +2,15 @@
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { env } from "@/env/client"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
import BookingCode from "../BookingCode"
import { RewardNight } from "../RewardNight"
import styles from "./voucher.module.css"
@@ -31,36 +34,44 @@ export default function Voucher() {
<BookingCode />
</div>
<div className={styles.options}>
<div className={styles.option}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{bonus}</span>
</Caption>
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</Tooltip>
</div>
<div className={styles.option}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{reward}</span>
</Caption>
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</Tooltip>
</div>
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? (
<>
<div className={styles.option}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{bonus}</span>
</Caption>
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</Tooltip>
</div>
<div className={styles.option}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{reward}</span>
</Caption>
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</Tooltip>
</div>
</>
) : (
<div className={styles.option}>
<RewardNight />
</div>
)}
</div>
</div>
)
@@ -87,16 +98,18 @@ export function VoucherSkeleton() {
<SkeletonShimmer width={"100%"} />
</div>
<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}>
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{bonus}</span>
</Caption>
</Checkbox>
</div>
<div className={styles.option}>
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<Checkbox name="redemption">
<Caption color="uiTextMediumContrast" asChild>
<span>{reward}</span>
</Caption>
</Checkbox>

View File

@@ -49,6 +49,8 @@ export default function Form({
...(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()

View File

@@ -91,7 +91,6 @@ export const bookingWidgetSchema = z
redemption: z.boolean().default(false),
rooms: guestRoomsSchema,
search: z.string({ coerce: true }).min(1, "Required"),
voucher: z.boolean().default(false),
hotel: z.number().optional(),
city: z.string().optional(),
})
@@ -130,4 +129,28 @@ export const bookingWidgetSchema = z
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"],
})
}
})

View File

@@ -37,9 +37,8 @@ export default function GuestsRoomsPickerDialog({
const roomsValue = useWatch<BookingWidgetSchema, "rooms">({ name: "rooms" })
const addRoomLabel = intl.formatMessage({ id: "Add room" })
const doneLabel = intl.formatMessage({ id: "Done" })
// Disable add room if booking code is voucher or reward night is enebaled
const addRoomDisabledText = getValues("redemption")
// Disable add room if booking code is either voucher or corporate cheque, or reward night is enabled
const addRoomDisabledTextForSpecialRate = getValues("redemption")
? intl.formatMessage({
id: "Multi-room booking is not available with reward night.",
})
@@ -102,39 +101,52 @@ export default function GuestsRoomsPickerDialog({
/>
))}
{canAddRooms && (
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.addRoomMobileContainer}>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
disabled={addRoomDisabledText ? true : false}
<Tooltip
text={addRoomDisabledTextForSpecialRate}
position="bottom"
arrow="left"
>
<PlusIcon />
{addRoomLabel}
</Button>
{addRoomDisabledText ? (
<Caption
color="blue"
className={styles.addRoomMobileDisabledText}
<Button
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
disabled
>
<ErrorCircleIcon color="blue" width={20} />
{addRoomDisabledText}
</Caption>
) : null}
<PlusIcon />
{addRoomLabel}
</Button>
</Tooltip>
</div>
) : (
canAddRooms && (
<div className={styles.addRoomMobileContainer}>
<Button
intent="text"
variant="icon"
wrapping
theme="base"
fullWidth
onPress={handleAddRoom}
>
<PlusIcon />
{addRoomLabel}
</Button>
</div>
)
)}
</div>
</section>
<footer className={styles.footer}>
{addRoomDisabledText ? (
{addRoomDisabledTextForSpecialRate ? (
<div className={styles.hideOnMobile}>
<Tooltip
text={addRoomDisabledText}
position={"bottom"}
text={addRoomDisabledTextForSpecialRate}
position="bottom"
arrow="left"
>
<Button
@@ -143,6 +155,7 @@ export default function GuestsRoomsPickerDialog({
wrapping
theme="base"
disabled
onPress={handleAddRoom}
>
<PlusCircleIcon />
{addRoomLabel}

View File

@@ -16,6 +16,7 @@ export default function Checkbox({
name,
children,
registerOptions,
hideError,
}: React.PropsWithChildren<CheckboxProps>) {
const { control } = useFormContext()
const { field, fieldState } = useController({
@@ -44,7 +45,7 @@ export default function Checkbox({
</span>
{children}
</span>
{fieldState.error ? (
{fieldState.error && !hideError ? (
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />
{fieldState.error.message}

View File

@@ -150,6 +150,7 @@
"Close the map": "Close the map",
"Closed": "Closed",
"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.",
"Coming up": "Coming up",
"Compare all levels": "Compare all levels",

View File

@@ -19,6 +19,7 @@ export type BookingWidgetSearchData = {
toDate?: string
rooms?: TGuestsRoom[]
bookingCode?: string
searchType?: "redemption"
}
export type BookingWidgetType = VariantProps<

View File

@@ -4,4 +4,5 @@ export interface CheckboxProps
extends React.InputHTMLAttributes<HTMLInputElement> {
name: string
registerOptions?: RegisterOptions
hideError?: boolean
}

View File

@@ -27,6 +27,7 @@ const keyedSearchParams = new Map([
["todate", "toDate"],
["hotel", "hotelId"],
["child", "childrenInRoom"],
["searchtype", "searchType"],
])
export type SelectHotelParams<T> = Omit<T, "hotel"> & {