feat: SW-963 Implemented error states and handling booking code and multiroom

This commit is contained in:
Hrishikesh Vaipurkar
2025-02-24 17:06:20 +01:00
parent 202d84218c
commit 2cd1b6c72c
16 changed files with 374 additions and 158 deletions

View File

@@ -1,5 +1,14 @@
.container {
position: relative;
display: grid;
gap: var(--Spacing-x1);
}
.bookingCode {
height: 60px;
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
}
.bookingCodeLabel {
@@ -9,6 +18,11 @@
position: relative;
}
.errorContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-half);
}
.error {
display: flex;
gap: var(--Spacing-x-half);
@@ -26,8 +40,6 @@
.bookingCodeRememberVisible {
display: flex;
position: absolute;
top: calc(100% + 16px);
width: 100%;
}
@@ -36,6 +48,10 @@
margin-top: var(--Spacing-x2);
}
.bookingCodeRememberVisible label {
align-items: center;
}
@media screen and (max-width: 767px) {
.hideOnMobile {
display: none;
@@ -43,6 +59,10 @@
}
@media screen and (min-width: 768px) {
.bookingCode {
height: auto;
background-color: transparent;
}
.bookingCodeRememberVisible {
align-items: center;
background: var(--Base-Surface-Primary-light-Normal);
@@ -54,7 +74,6 @@
@media screen and (min-width: 768px) and (max-width: 1367px) {
.container {
display: flex;
gap: var(--Spacing-x1);
}
.codePopover {
background: var(--Base-Surface-Primary-light-Normal);
@@ -67,16 +86,30 @@
display: grid;
gap: var(--Spacing-x2);
}
.bookingCodeRememberVisible {
position: static;
.overlayTrigger {
position: absolute;
top: 0;
bottom: 0;
display: block;
left: 0;
right: 24px;
}
}
@media screen and (min-width: 1367px) {
.container:hover,
.container:focus-within,
.container:has([data-focused="true"], [data-pressed="true"]) {
background-color: var(--Base-Surface-Primary-light-Hover-alt);
border-radius: var(--Corner-radius-Medium);
}
.bookingCodeRememberVisible {
padding: var(--Spacing-x2);
width: 320px;
position: absolute;
top: calc(100% + 24px);
left: calc(0% - var(--Spacing-x-one-and-half));
left: calc(0% - var(--Spacing-x-half));
width: 360px;
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
}
}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { Dialog, DialogTrigger, Popover } from "react-aria-components"
import { useFormContext } from "react-hook-form"
import { type FieldError,useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
@@ -21,6 +21,7 @@ import type {
BookingCodeSchema,
BookingWidgetSchema,
} from "@/types/components/bookingWidget"
import type { ButtonProps } from "@/components/TempDesignSystem/Button/button"
export default function BookingCode() {
const intl = useIntl()
@@ -34,11 +35,9 @@ export default function BookingCode() {
setValue,
formState: { errors },
getValues,
register,
} = useFormContext<BookingWidgetSchema>()
const bookingCode: BookingCodeSchema = getValues("bookingCode")
const [isOpen, setIsOpen] = useState(!!bookingCode?.value)
const [showRemember, setShowRemember] = useState(false)
const [showRememberMobile, setShowRememberMobile] = useState(false)
const codeError = errors["bookingCode"]?.value
@@ -50,24 +49,21 @@ export default function BookingCode() {
setValue("bookingCode.value", value, { shouldValidate: true })
}
function toggleModal(isOpen: boolean) {
if (!isOpen && !bookingCode?.value) {
setValue("bookingCode.flag", false)
setIsOpen(isOpen)
} else if (!codeError || isOpen) {
setIsOpen(isOpen)
if (isOpen || bookingCode?.value) {
setValue("bookingCode.flag", true)
}
}
}
const closeIfOutside = useCallback(
(target: HTMLElement) => {
if (ref.current && target && !ref.current.contains(target)) {
if (
ref.current &&
target &&
!ref.current.contains(target) &&
target.getAttribute("value") !== "Remove extra rooms"
) {
setShowRemember(false)
if (codeError) {
setValue("bookingCode.value", "", { shouldValidate: true })
}
}
},
[setShowRemember, ref]
[setShowRemember, setValue, ref, codeError]
)
function showRememberCheck() {
@@ -102,46 +98,10 @@ export default function BookingCode() {
}, [closeIfOutside, showRemember])
return isTablet ? (
<div className={styles.container}>
<DialogTrigger isOpen={isOpen} onOpenChange={toggleModal}>
<Button type="button" intent="text">
<Checkbox
checked={!!bookingCode?.value}
{...register("bookingCode.flag", {
onChange: function () {
if (bookingCode?.value || isOpen) {
setValue("bookingCode.flag", true)
}
},
})}
>
<Caption color="uiTextMediumContrast" asChild>
<span>{codeVoucher}</span>
</Caption>
</Checkbox>
</Button>
<Popover
className={styles.codePopover}
placement="bottom start"
offset={36}
>
<Dialog>
{({ close }) => (
<div className={styles.popover}>
<TabletCodeInput updateValue={updateBookingCodeFormValue} />
<div className={styles.bookingCodeRememberVisible}>
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={close}
/>
</div>
</div>
)}
</Dialog>
</Popover>
</DialogTrigger>
<CodeRulesModal />
</div>
<TabletBookingCode
bookingCode={bookingCode}
updateValue={updateBookingCodeFormValue}
/>
) : (
<div
className={styles.container}
@@ -149,32 +109,28 @@ export default function BookingCode() {
onFocus={showRememberCheck}
onBlur={(e) => closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)}
>
<div className={styles.bookingCodeLabel}>
<Caption
color={showRemember ? "uiTextActive" : "red"}
type="bold"
asChild
>
<span>{codeVoucher}</span>
</Caption>
<CodeRulesModal />
<div className={styles.bookingCode}>
<div className={styles.bookingCodeLabel}>
<Caption
color={showRemember ? "uiTextActive" : "red"}
type="bold"
asChild
>
<span>{codeVoucher}</span>
</Caption>
<CodeRulesModal />
</div>
<Input
className="input"
type="search"
placeholder={addCode}
name="bookingCode.value"
id="booking-code"
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
autoComplete="off"
value={bookingCode?.value}
/>
</div>
<Input
className="input"
type="search"
placeholder={addCode}
name="bookingCode.value"
id="booking-code"
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
defaultValue={bookingCode?.value}
autoComplete="off"
/>
{codeError?.message ? (
<Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" className={styles.errorIcon} />
{intl.formatMessage({ id: codeError.message })}
</Caption>
) : null}
{isDesktop ? (
<div
className={
@@ -183,25 +139,35 @@ export default function BookingCode() {
: styles.bookingCodeRemember
}
>
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={() => setShowRemember(false)}
/>
{codeError?.message ? (
<BookingCodeError codeError={codeError} />
) : (
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={() => setShowRemember(false)}
/>
)}
</div>
) : (
<div
className={
showRememberMobile
? styles.bookingCodeRememberVisible
: styles.bookingCodeRemember
}
>
<Switch name="bookingCode.remember" className="mobile-switch">
<Caption asChild>
<span>{intl.formatMessage({ id: "Remember code" })}</span>
</Caption>
</Switch>
</div>
<>
{codeError?.message ? (
<BookingCodeError codeError={codeError} />
) : (
<div
className={
showRememberMobile
? styles.bookingCodeRememberVisible
: styles.bookingCodeRemember
}
>
<Switch name="bookingCode.remember" className="mobile-switch">
<Caption asChild>
<span>{intl.formatMessage({ id: "Remember code" })}</span>
</Caption>
</Switch>
</div>
)}
</>
)}
</div>
)
@@ -259,3 +225,128 @@ function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) {
</>
)
}
function BookingCodeError({ codeError }: { codeError: FieldError }) {
const intl = useIntl()
const isMultiroomErr = codeError.message?.indexOf("Multi-room") !== -1
return (
<div className={styles.errorContainer}>
<Caption color={isMultiroomErr ? "blue" : "red"} className={styles.error}>
<ErrorCircleIcon
color={isMultiroomErr ? "blue" : "red"}
className={styles.errorIcon}
/>
{intl.formatMessage({ id: codeError.message })}
</Caption>
{isMultiroomErr ? (
<RemoveExtraRooms className={styles.hideOnMobile} />
) : null}
</div>
)
}
export function RemoveExtraRooms({ ...props }: ButtonProps) {
const intl = useIntl()
const { getValues, setValue, trigger } = useFormContext<BookingWidgetSchema>()
function removeExtraRooms() {
// Timeout to delay the event scheduling issue with touch events on mobile
window.setTimeout(() => {
const rooms = getValues("rooms")[0]
setValue("rooms", [rooms], { shouldValidate: true })
trigger("bookingCode.value")
}, 300)
}
return (
<Button
value="Remove extra rooms"
type="button"
onClick={removeExtraRooms}
size="small"
intent="secondary"
{...props}
>
{intl.formatMessage({ id: "Remove extra rooms" })}
</Button>
)
}
function TabletBookingCode({
bookingCode,
updateValue,
}: {
bookingCode: BookingCodeSchema
updateValue: (value: string) => void
}) {
const intl = useIntl()
const [isOpen, setIsOpen] = useState(!!bookingCode?.value)
const {
setValue,
register,
formState: { errors },
} = useFormContext<BookingWidgetSchema>()
const codeError = errors["bookingCode"]?.value
const codeVoucher = intl.formatMessage({ id: "Code / Voucher" })
function toggleModal(isOpen: boolean) {
if (!isOpen && !bookingCode?.value) {
setValue("bookingCode.flag", false)
setIsOpen(isOpen)
} else if (!codeError || isOpen) {
setIsOpen(isOpen)
if (isOpen || bookingCode?.value) {
setValue("bookingCode.flag", true)
}
}
}
return (
<div className={styles.container}>
<DialogTrigger isOpen={isOpen} onOpenChange={toggleModal}>
<Button type="button" intent="text">
{/* For some reason Checkbox click triggers twice modal state change, which returns the modal back to old state. So we are using overlay as trigger for modal */}
<div className={styles.overlayTrigger}></div>
<Checkbox
checked={!!bookingCode?.value}
{...register("bookingCode.flag", {
onChange: function () {
if (bookingCode?.value || isOpen) {
setValue("bookingCode.flag", true)
}
},
})}
>
<Caption color="uiTextMediumContrast" type="bold" asChild>
<span>{codeVoucher}</span>
</Caption>
</Checkbox>
</Button>
<Popover
className={styles.codePopover}
placement="bottom start"
offset={36}
>
<Dialog>
{({ close }) => (
<div className={styles.popover}>
<TabletCodeInput updateValue={updateValue} />
<div className={styles.bookingCodeRememberVisible}>
{codeError?.message ? (
<RemoveExtraRooms />
) : (
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={close}
/>
)}
</div>
</div>
)}
</Dialog>
</Popover>
</DialogTrigger>
<CodeRulesModal />
</div>
)
}

View File

@@ -11,12 +11,6 @@
margin-top: var(--Spacing-x2);
align-items: center;
}
.vouchers {
width: 100%;
display: block;
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
border-radius: var(--Corner-radius-Small);
}
.optionsContainer {
display: flex;
@@ -28,12 +22,6 @@
height: 24px;
}
@media screen and (max-width: 767px) {
.vouchers {
margin-bottom: var(--Spacing-x5);
}
}
@media screen and (min-width: 768px) {
.options {
flex-direction: row;
@@ -48,25 +36,9 @@
grid-template-columns: auto auto;
column-gap: var(--Spacing-x2);
}
.vouchers:hover,
.vouchers:focus-within,
.vouchers:has([data-focused="true"], [data-pressed="true"]) {
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
}
@media screen and (max-width: 1366px) {
.vouchers {
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
}
}
@media screen and (min-width: 1367px) {
.vouchers {
display: block;
max-width: 200px;
}
.options {
flex-direction: column;
max-width: 190px;

View File

@@ -16,6 +16,11 @@
position: relative;
}
.buttonContainer {
display: grid;
gap: var(--Spacing-x1);
}
.showOnTablet {
display: none;
}
@@ -33,7 +38,6 @@
}
.rooms,
.vouchers,
.when,
.where {
background-color: var(--Base-Background-Primary-Normal);
@@ -41,7 +45,6 @@
}
.rooms,
.vouchers,
.when {
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
}
@@ -103,6 +106,10 @@
justify-content: center;
width: 118px;
}
.showOnMobile {
display: none;
}
}
@media screen and (min-width: 768px) and (max-width: 1366px) {
@@ -143,3 +150,9 @@
display: none;
}
}
@media screen and (min-width: 1366px) {
.input {
gap: var(--Spacing-x2);
}
}

View File

@@ -1,5 +1,5 @@
"use client"
import { useWatch } from "react-hook-form"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
@@ -11,11 +11,13 @@ import SkeletonShimmer from "@/components/SkeletonShimmer"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { RemoveExtraRooms } from "./BookingCode"
import Search, { SearchSkeleton } from "./Search"
import Voucher, { VoucherSkeleton } from "./Voucher"
import styles from "./formContent.module.css"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import type { BookingWidgetFormContentProps } from "@/types/components/form/bookingwidget"
export default function FormContent({
@@ -25,6 +27,10 @@ export default function FormContent({
isSearching,
}: BookingWidgetFormContentProps) {
const intl = useIntl()
const {
formState: { errors },
} = useFormContext<BookingWidgetSchema>()
const bookingCodeError = errors["bookingCode"]?.value
const selectedDate = useWatch({ name: "date" })
const roomsLabel = intl.formatMessage({ id: "Rooms & Guests" })
@@ -79,6 +85,13 @@ export default function FormContent({
<Voucher />
</div>
<div className={`${styles.buttonContainer} ${styles.hideOnTablet}`}>
{bookingCodeError?.message?.indexOf("Multi-room") === 0 ? (
<RemoveExtraRooms
size="medium"
fullWidth
className={styles.showOnMobile}
/>
) : null}
<Button
className={styles.button}
form={formId}

View File

@@ -109,12 +109,24 @@ export const bookingWidgetSchema = z
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multiroom with voucher error",
message: "Multi-room booking is not available with this booking code.",
path: ["bookingCode.value"],
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multiroom with voucher error",
message: "Multi-room booking is not available with this booking code.",
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: ["bookingCode.value"],
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Multi-room booking is not available with reward night.",
path: ["rooms"],
})
}