feat: SW-1422 Enabled booking code in booking widget

This commit is contained in:
Hrishikesh Vaipurkar
2025-01-23 20:25:37 +01:00
parent 46ebbbba8f
commit 1b5b09d7a6
16 changed files with 256 additions and 47 deletions

View File

@@ -23,6 +23,7 @@ import MobileToggleButton, {
import styles from "./bookingWidget.module.css"
import type {
BookingCodeSchema,
BookingWidgetClientProps,
BookingWidgetSchema,
BookingWidgetSearchData,
@@ -82,6 +83,10 @@ export default function BookingWidgetClient({
)
: undefined
const selectedBookingCode = bookingWidgetSearchData
? bookingWidgetSearchData.bookingCode
: undefined
const defaultRoomsData: BookingWidgetSchema["rooms"] =
bookingWidgetSearchData?.rooms?.map((room) => ({
adults: room.adults,
@@ -107,7 +112,10 @@ export default function BookingWidgetClient({
? parsedToDate.format("YYYY-MM-DD")
: now.utc().add(1, "day").format("YYYY-MM-DD"),
},
bookingCode: "",
bookingCode: {
value: selectedBookingCode ?? "",
remember: false,
},
redemption: false,
voucher: false,
rooms: defaultRoomsData,
@@ -164,7 +172,20 @@ export default function BookingWidgetClient({
!selectedLocation &&
sessionStorageSearchData &&
methods.setValue("location", encodeURIComponent(sessionStorageSearchData))
}, [methods, selectedLocation])
const storedBookingCode =
typeof window !== "undefined"
? localStorage.getItem("bookingCode")
: undefined
const initialBookingCode: BookingCodeSchema | undefined =
storedBookingCode && isValidJson(storedBookingCode)
? JSON.parse(storedBookingCode)
: undefined
!selectedBookingCode &&
initialBookingCode &&
initialBookingCode.remember &&
methods.setValue("bookingCode", initialBookingCode)
}, [methods, selectedLocation, selectedBookingCode])
return (
<FormProvider {...methods}>

View File

@@ -0,0 +1,22 @@
.bookingCodeLabel {
display: flex;
align-items: center;
gap: var(--Spacing-x-half);
}
.error {
display: flex;
gap: var(--Spacing-x-half);
}
.errorIcon {
min-width: 20px;
}
.bookingCodeRemember {
display: flex;
gap: var(--Spacing-x1);
/* ToDo: Remove once remember checkbox design are ready */
display: none;
}

View File

@@ -0,0 +1,90 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { ErrorCircleIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Input from "../Input"
import styles from "./booking-code.module.css"
import type {
BookingCodeSchema,
BookingWidgetSchema,
} from "@/types/components/bookingWidget"
export default function BookingCode() {
const intl = useIntl()
const {
setValue,
formState: { errors },
getValues,
} = useFormContext<BookingWidgetSchema>()
const codeError = errors["bookingCode"]?.value
const codeVoucher = intl.formatMessage({ id: "Code / Voucher" })
const addCode = intl.formatMessage({ id: "Add code" })
const bookingCodeTooltipText = intl.formatMessage({
id: "booking.codes.information",
})
const bookingCode: BookingCodeSchema = getValues("bookingCode")
function updateBookingCodeFormValue(value: string) {
setValue("bookingCode.value", value, { shouldValidate: true })
}
return (
<>
<label htmlFor="booking-code">
<div className={styles.bookingCodeLabel}>
<Caption color="uiTextMediumContrast" type="bold" asChild>
<span>{codeVoucher}</span>
</Caption>
<Modal
trigger={
<Button intent="text">
<InfoCircleIcon
color="uiTextPlaceholder"
height={20}
width={20}
/>
</Button>
}
title={codeVoucher}
>
<Body color="uiTextHighContrast">{bookingCodeTooltipText}</Body>
</Modal>
</div>
<Input
type="text"
placeholder={addCode}
name="bookingCode"
id="booking-code"
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
defaultValue={bookingCode?.value}
/>
</label>
{codeError && codeError.message ? (
<Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" className={styles.errorIcon} />
{intl.formatMessage({ id: codeError.message })}
</Caption>
) : null}
{/* ToDo: Update styles once designs are ready */}
<div className={styles.bookingCodeRemember}>
<Checkbox name="bookingCode.remember">
<Caption asChild>
<span>{intl.formatMessage({ id: "Remember code" })}</span>
</Caption>
</Checkbox>
<Button size="small" intent="primary" type="button">
{intl.formatMessage({ id: "Apply" })}
</Button>
</div>
</>
)
}

View File

@@ -6,6 +6,7 @@ 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 Input from "../Input"
import styles from "./voucher.module.css"
@@ -13,11 +14,11 @@ import styles from "./voucher.module.css"
export default function Voucher() {
const intl = useIntl()
const vouchers = intl.formatMessage({ id: "Code / Voucher" })
const useVouchers = intl.formatMessage({ id: "Use code/voucher" })
const addVouchers = intl.formatMessage({ id: "Add code" })
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
const reward = intl.formatMessage({ id: "Book reward night" })
// ToDo: Remove this when all three options are enabled
const disabledBookingOptionsHeader = intl.formatMessage({
id: "We're sorry",
})
@@ -27,55 +28,49 @@ export default function Voucher() {
return (
<div className={styles.optionsContainer}>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<div className={styles.vouchers}>
<label>
<Caption color="disabled" type="bold" asChild>
<span>{vouchers}</span>
<div className={styles.vouchers}>
<BookingCode />
</div>
<div className={styles.options}>
<div className={`${styles.option} ${styles.checkboxVoucher}`}>
<Checkbox name="useVouchers" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{useVouchers}</span>
</Caption>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</label>
<Input type="text" placeholder={addVouchers} disabled />
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</div>
</Tooltip>
<Tooltip
heading={disabledBookingOptionsHeader}
text={disabledBookingOptionsText}
position="bottom"
arrow="left"
>
<div className={styles.options}>
<div className={`${styles.option} ${styles.checkboxVoucher}`}>
<Checkbox name="useVouchers" registerOptions={{ disabled: true }}>
<Caption color="disabled" asChild>
<span>{useVouchers}</span>
</Caption>
</Checkbox>
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
</div>
<div className={styles.option}>
<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 */}
</div>
<div className={styles.option}>
</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 */}
</div>
</Tooltip>
</div>
</Tooltip>
</div>
</div>
)
}

View File

@@ -32,10 +32,6 @@
display: none;
}
.infoIcon {
stroke: var(--Base-Text-Disabled);
}
@media screen and (min-width: 768px) {
.vouchers {
display: none;

View File

@@ -86,6 +86,7 @@ export default function FormContent({
</Button>
</div>
</div>
{/* ToDo: Remove below additional voucher element which is only used in tablet mode better to have submit button twice instead */}
<div className={styles.voucherRow}>
<Voucher />
</div>

View File

@@ -44,10 +44,16 @@ export default function Form({
...(locationData.type == "cities"
? { city: locationData.name }
: { hotel: locationData.operaId || "" }),
...(data.bookingCode && data.bookingCode.value
? { bookingCode: data.bookingCode.value }
: {}),
})
onClose()
router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`)
if (data.bookingCode?.remember) {
localStorage.setItem("bookingCode", JSON.stringify(data.bookingCode))
}
}
return (

View File

@@ -35,9 +35,30 @@ export const guestRoomSchema = z
export const guestRoomsSchema = z.array(guestRoomSchema)
export const bookingCodeSchema = z
.object({
value: z.string().refine(
(value) => {
if (
!value ||
/(^D\d*$)|(^DSH[0-9a-z]*$)|(^L\d*$)|(^LH[0-9a-z]*$)|(^B[a-z]{3}\d{6})|(^VO[0-9a-z]*$)|^[0-9a-z]*$/i.test(
value
)
) {
return true
} else {
return false
}
},
{ message: "Invalid booking code" }
),
remember: z.boolean(),
})
.optional()
export const bookingWidgetSchema = z
.object({
bookingCode: z.string(), // Update this as required when working with booking codes component
bookingCode: bookingCodeSchema,
date: z.object({
// Update this as required once started working with Date picker in Nights component
fromDate: z.string(),
@@ -70,7 +91,27 @@ export const bookingWidgetSchema = z
hotel: z.number().optional(),
city: z.string().optional(),
})
.refine((value) => value.hotel || value.city, {
message: "Destination required",
path: ["search"],
.superRefine((value, ctx) => {
if (!value.hotel && !value.city) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Destination required",
path: ["search"],
})
}
if (
value.rooms.length > 1 &&
value.bookingCode?.value.toLowerCase().startsWith("vo")
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multiroom with voucher error",
path: ["bookingCode.value"],
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multiroom with voucher error",
path: ["rooms"],
})
}
})

View File

@@ -76,5 +76,6 @@
bottom: auto;
width: auto;
border-radius: var(--Corner-radius-Medium);
max-width: var(--max-width-page);
}
}