feat: SW-963 Implemented error states and handling booking code and multiroom
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"],
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user