Merged in fix/BOOK-323-enter-details-scroll-error (pull request #2986)
Fix/BOOK-323 enter details scroll error * fix(BOOK-323): scroll to invalid element on submit on enter details * fix(BOOK-323): update error message design * fix(BOOK-323): clean up * fix(BOOK-323): scroll to fields in room in right order * fix(BOOK-323): add id to translations * fix(BOOK-323): remove undefined * fix(BOOK-323): fix submitting state * fix(BOOK-323): use ref in multiroom for scrolling to right element, add membershipNo * fix(BOOK-323): fix invalid border country * fix(BOOK-323): use error message component * fix(BOOK-323): fix invalid focused styling on mobile * fix(BOOK-323): remove redundant dependency in callback Approved-by: Erik Tiekstra
This commit is contained in:
@@ -6,6 +6,7 @@ import { Controller, useFormContext } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from "@scandic-hotels/design-system/Caption"
|
||||||
|
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Input as InputWithLabel } from "@scandic-hotels/design-system/Input"
|
import { Input as InputWithLabel } from "@scandic-hotels/design-system/Input"
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { control } = useFormContext()
|
const { control, formState } = useFormContext()
|
||||||
const numberAttributes: HTMLAttributes<HTMLInputElement> = {}
|
const numberAttributes: HTMLAttributes<HTMLInputElement> = {}
|
||||||
if (type === "number") {
|
if (type === "number") {
|
||||||
numberAttributes.onWheel = function (evt: WheelEvent<HTMLInputElement>) {
|
numberAttributes.onWheel = function (evt: WheelEvent<HTMLInputElement>) {
|
||||||
@@ -87,10 +88,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
</Caption>
|
</Caption>
|
||||||
) : null}
|
) : null}
|
||||||
{fieldState.error && !hideError ? (
|
{fieldState.error && !hideError ? (
|
||||||
<Caption className={styles.error} fontOnly>
|
<ErrorMessage
|
||||||
<MaterialIcon icon="info" color="Icon/Feedback/Error" />
|
errors={formState.errors}
|
||||||
{getErrorMessage(intl, fieldState.error.message)}
|
name={name}
|
||||||
</Caption>
|
messageLabel={getErrorMessage(intl, fieldState.error.message)}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TextField>
|
</TextField>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
import Caption from "@scandic-hotels/design-system/Caption"
|
||||||
|
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Input as InputWithLabel } from "@scandic-hotels/design-system/Input"
|
import { Input as InputWithLabel } from "@scandic-hotels/design-system/Input"
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ const BookingFlowInput = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { control } = useFormContext()
|
const { control, formState } = useFormContext()
|
||||||
const config = useBookingFlowConfig()
|
const config = useBookingFlowConfig()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,14 +97,15 @@ const BookingFlowInput = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
</Caption>
|
</Caption>
|
||||||
) : null}
|
) : null}
|
||||||
{fieldState.error && !hideError ? (
|
{fieldState.error && !hideError ? (
|
||||||
<Caption className={styles.error} fontOnly>
|
<ErrorMessage
|
||||||
<MaterialIcon icon="info" color="Icon/Feedback/Error" />
|
errors={formState.errors}
|
||||||
{getErrorMessage(
|
name={name}
|
||||||
|
messageLabel={getErrorMessage(
|
||||||
intl,
|
intl,
|
||||||
config.variant,
|
config.variant,
|
||||||
fieldState.error.message
|
fieldState.error.message
|
||||||
)}
|
)}
|
||||||
</Caption>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TextField>
|
</TextField>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,15 +3,3 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x05);
|
gap: var(--Space-x05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
|
||||||
align-items: center;
|
|
||||||
color: var(--Text-Interactive-Error);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x05);
|
|
||||||
margin: var(--Space-x1) 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error svg {
|
|
||||||
min-width: 20px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,3 +15,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-one-and-half);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorContainer {
|
||||||
|
width: min(696px, 100%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import RadioCard from "@scandic-hotels/design-system/Form/RadioCard"
|
import RadioCard from "@scandic-hotels/design-system/Form/RadioCard"
|
||||||
|
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
|
||||||
import { trackBedSelection } from "@scandic-hotels/tracking/booking"
|
import { trackBedSelection } from "@scandic-hotels/tracking/booking"
|
||||||
import {
|
import {
|
||||||
BedTypeEnum,
|
BedTypeEnum,
|
||||||
@@ -21,11 +23,20 @@ import styles from "./bedOptions.module.css"
|
|||||||
import type { IconProps } from "@scandic-hotels/design-system/Icons"
|
import type { IconProps } from "@scandic-hotels/design-system/Icons"
|
||||||
|
|
||||||
export default function BedType() {
|
export default function BedType() {
|
||||||
const availableBeds = useEnterDetailsStore((state) => state.availableBeds)
|
|
||||||
const {
|
const {
|
||||||
actions: { updateBedType },
|
actions: { updateBedType },
|
||||||
room: { bedType, bedTypes },
|
room: { bedType, bedTypes },
|
||||||
|
idx,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
|
const { addPreSubmitCallback, availableBeds } = useEnterDetailsStore(
|
||||||
|
(state) => ({
|
||||||
|
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
||||||
|
availableBeds: state.availableBeds,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const intl = useIntl()
|
||||||
|
const formRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const initialBedType = bedType?.roomTypeCode
|
const initialBedType = bedType?.roomTypeCode
|
||||||
const [previousBedType, setPreviousBedType] = useState("")
|
const [previousBedType, setPreviousBedType] = useState("")
|
||||||
|
|
||||||
@@ -56,6 +67,17 @@ export default function BedType() {
|
|||||||
[bedTypes, updateBedType]
|
[bedTypes, updateBedType]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function callback() {
|
||||||
|
const isValid = await methods.trigger()
|
||||||
|
if (!isValid && methods.formState.errors.bedType) {
|
||||||
|
return formRef.current ?? undefined
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addPreSubmitCallback(`${idx}-bedtype`, callback)
|
||||||
|
}, [addPreSubmitCallback, methods, idx])
|
||||||
|
|
||||||
const selectedBedType = methods.watch("bedType")
|
const selectedBedType = methods.watch("bedType")
|
||||||
const handleSubmit = methods.handleSubmit
|
const handleSubmit = methods.handleSubmit
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,7 +95,19 @@ export default function BedType() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container} ref={formRef}>
|
||||||
|
<div className={styles.errorContainer}>
|
||||||
|
{methods.formState.errors.bedType && (
|
||||||
|
<MessageBanner
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "enterDetails.bedType.error.required",
|
||||||
|
defaultMessage: "Bed preference is required",
|
||||||
|
})}
|
||||||
|
type="error"
|
||||||
|
textColor="error"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
{bedTypes.map((roomType) => {
|
{bedTypes.map((roomType) => {
|
||||||
const width =
|
const width =
|
||||||
|
|||||||
@@ -10,3 +10,7 @@
|
|||||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||||
width: min(696px, 100%);
|
width: min(696px, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorContainer {
|
||||||
|
width: min(696px, 100%);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useCallback, useEffect } from "react"
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import Body from "@scandic-hotels/design-system/Body"
|
|||||||
import RadioCard from "@scandic-hotels/design-system/Form/RadioCard"
|
import RadioCard from "@scandic-hotels/design-system/Form/RadioCard"
|
||||||
import BreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/BreakfastBuffetIcon"
|
import BreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/BreakfastBuffetIcon"
|
||||||
import NoBreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/NoBreakfastBuffetIcon"
|
import NoBreakfastBuffetIcon from "@scandic-hotels/design-system/Icons/NoBreakfastBuffetIcon"
|
||||||
|
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
|
||||||
import { trackBreakfastSelection } from "@scandic-hotels/tracking/booking"
|
import { trackBreakfastSelection } from "@scandic-hotels/tracking/booking"
|
||||||
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
|
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
|
||||||
|
|
||||||
@@ -21,11 +22,16 @@ import styles from "./breakfast.module.css"
|
|||||||
|
|
||||||
export default function Breakfast() {
|
export default function Breakfast() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const formRef = useRef<HTMLDivElement>(null)
|
||||||
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
|
const packages = useEnterDetailsStore((state) => state.breakfastPackages)
|
||||||
const hotelId = useEnterDetailsStore((state) => state.booking.hotelId)
|
const hotelId = useEnterDetailsStore((state) => state.booking.hotelId)
|
||||||
|
const { addPreSubmitCallback } = useEnterDetailsStore((state) => ({
|
||||||
|
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
||||||
|
}))
|
||||||
const {
|
const {
|
||||||
actions: { updateBreakfast },
|
actions: { updateBreakfast },
|
||||||
room,
|
room,
|
||||||
|
idx,
|
||||||
} = useRoomContext()
|
} = useRoomContext()
|
||||||
|
|
||||||
const hasChildrenInRoom = !!room.childrenInRoom?.length
|
const hasChildrenInRoom = !!room.childrenInRoom?.length
|
||||||
@@ -65,6 +71,19 @@ export default function Breakfast() {
|
|||||||
[packages, hotelId, room.adults, updateBreakfast]
|
[packages, hotelId, room.adults, updateBreakfast]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function callback() {
|
||||||
|
const isValid = await methods.trigger()
|
||||||
|
if (!isValid && methods.formState.errors.breakfast) {
|
||||||
|
return formRef.current ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addPreSubmitCallback(`${idx}-breakfast`, callback)
|
||||||
|
}, [addPreSubmitCallback, methods, idx])
|
||||||
|
|
||||||
const selectedBreakfast = methods.watch("breakfast")
|
const selectedBreakfast = methods.watch("breakfast")
|
||||||
const handleSubmit = methods.handleSubmit
|
const handleSubmit = methods.handleSubmit
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,7 +94,7 @@ export default function Breakfast() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container} ref={formRef}>
|
||||||
{hasChildrenInRoom ? (
|
{hasChildrenInRoom ? (
|
||||||
<Body>
|
<Body>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
@@ -85,6 +104,19 @@ export default function Breakfast() {
|
|||||||
})}
|
})}
|
||||||
</Body>
|
</Body>
|
||||||
) : null}
|
) : null}
|
||||||
|
{methods.formState.errors.breakfast && (
|
||||||
|
<div className={styles.errorContainer}>
|
||||||
|
<MessageBanner
|
||||||
|
text={intl.formatMessage({
|
||||||
|
id: "enterDetails.breakfast.error.required",
|
||||||
|
defaultMessage: "Breakfast option is required",
|
||||||
|
})}
|
||||||
|
type="error"
|
||||||
|
textColor="error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
{packages?.map((pkg) => (
|
{packages?.map((pkg) => (
|
||||||
<RadioCard
|
<RadioCard
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useCallback, useEffect, useMemo } from "react"
|
import { useCallback, useEffect, useMemo, useRef } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ export default function Details() {
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const config = useBookingFlowConfig()
|
const config = useBookingFlowConfig()
|
||||||
|
const refs = useRef<Record<string, HTMLElement | null>>({})
|
||||||
|
|
||||||
const { addPreSubmitCallback, rooms } = useEnterDetailsStore((state) => ({
|
const { addPreSubmitCallback, rooms } = useEnterDetailsStore((state) => ({
|
||||||
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
addPreSubmitCallback: state.actions.addPreSubmitCallback,
|
||||||
@@ -106,12 +107,32 @@ export default function Details() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function callback() {
|
async function callback() {
|
||||||
trigger()
|
await trigger()
|
||||||
trackFormSubmit()
|
trackFormSubmit()
|
||||||
|
const fieldOrder = [
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
"countryCode",
|
||||||
|
"email",
|
||||||
|
"phoneNumber",
|
||||||
|
"membershipNo",
|
||||||
|
]
|
||||||
|
for (const name of fieldOrder) {
|
||||||
|
const fieldError =
|
||||||
|
methods.formState.errors[
|
||||||
|
name as keyof typeof methods.formState.errors
|
||||||
|
]
|
||||||
|
if (fieldError && refs.current[name]) {
|
||||||
|
return refs.current[name] ?? undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addPreSubmitCallback(`${idx}-details`, callback)
|
addPreSubmitCallback(`${idx}-details`, callback)
|
||||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit])
|
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit, methods])
|
||||||
|
|
||||||
const updateDetailsStore = useCallback(() => {
|
const updateDetailsStore = useCallback(() => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@@ -188,88 +209,124 @@ export default function Details() {
|
|||||||
defaultMessage: "Guest information",
|
defaultMessage: "Guest information",
|
||||||
})}
|
})}
|
||||||
</Footnote>
|
</Footnote>
|
||||||
<BookingFlowInput
|
<div
|
||||||
label={intl.formatMessage({
|
ref={(el) => {
|
||||||
id: "common.firstName",
|
refs.current.firstName = el
|
||||||
defaultMessage: "First name",
|
|
||||||
})}
|
|
||||||
maxLength={30}
|
|
||||||
name="firstName"
|
|
||||||
registerOptions={{
|
|
||||||
required: true,
|
|
||||||
deps: "lastName",
|
|
||||||
onBlur: updateDetailsStore,
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<BookingFlowInput
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "common.lastName",
|
|
||||||
defaultMessage: "Last name",
|
|
||||||
})}
|
|
||||||
maxLength={30}
|
|
||||||
name="lastName"
|
|
||||||
registerOptions={{
|
|
||||||
required: true,
|
|
||||||
deps: "firstName",
|
|
||||||
onBlur: updateDetailsStore,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CountrySelect
|
|
||||||
className={styles.fullWidth}
|
|
||||||
countries={getFormattedCountryList(intl)}
|
|
||||||
errorMessage={getErrorMessage(
|
|
||||||
intl,
|
|
||||||
config.variant,
|
|
||||||
errors.countryCode?.message
|
|
||||||
)}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "common.country",
|
|
||||||
defaultMessage: "Country",
|
|
||||||
})}
|
|
||||||
lang={lang}
|
|
||||||
name="countryCode"
|
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
|
||||||
/>
|
|
||||||
<BookingFlowInput
|
|
||||||
className={styles.fullWidth}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "common.emailAddress",
|
|
||||||
defaultMessage: "Email address",
|
|
||||||
})}
|
|
||||||
name="email"
|
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
|
||||||
/>
|
|
||||||
<Phone
|
|
||||||
countryLabel={intl.formatMessage({
|
|
||||||
id: "common.countryCode",
|
|
||||||
defaultMessage: "Country code",
|
|
||||||
})}
|
|
||||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
|
||||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
|
||||||
errorMessage={getErrorMessage(
|
|
||||||
intl,
|
|
||||||
config.variant,
|
|
||||||
errors.phoneNumber?.message
|
|
||||||
)}
|
|
||||||
className={styles.fullWidth}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "common.phoneNumber",
|
|
||||||
defaultMessage: "Phone number",
|
|
||||||
})}
|
|
||||||
name="phoneNumber"
|
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
|
||||||
/>
|
|
||||||
{showMembershipIdInput ? (
|
|
||||||
<BookingFlowInput
|
<BookingFlowInput
|
||||||
className={styles.fullWidth}
|
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: "common.membershipId",
|
id: "common.firstName",
|
||||||
defaultMessage: "Membership ID",
|
defaultMessage: "First name",
|
||||||
})}
|
})}
|
||||||
name="membershipNo"
|
maxLength={30}
|
||||||
type="tel"
|
name="firstName"
|
||||||
registerOptions={{ onBlur: updateDetailsStore }}
|
registerOptions={{
|
||||||
|
required: true,
|
||||||
|
deps: "lastName",
|
||||||
|
onBlur: updateDetailsStore,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.lastName = el
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BookingFlowInput
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.lastName",
|
||||||
|
defaultMessage: "Last name",
|
||||||
|
})}
|
||||||
|
maxLength={30}
|
||||||
|
name="lastName"
|
||||||
|
registerOptions={{
|
||||||
|
required: true,
|
||||||
|
deps: "firstName",
|
||||||
|
onBlur: updateDetailsStore,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.countryCode = el
|
||||||
|
}}
|
||||||
|
className={styles.fullWidth}
|
||||||
|
>
|
||||||
|
<CountrySelect
|
||||||
|
countries={getFormattedCountryList(intl)}
|
||||||
|
errorMessage={getErrorMessage(
|
||||||
|
intl,
|
||||||
|
config.variant,
|
||||||
|
errors.countryCode?.message
|
||||||
|
)}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.country",
|
||||||
|
defaultMessage: "Country",
|
||||||
|
})}
|
||||||
|
lang={lang}
|
||||||
|
name="countryCode"
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.email = el
|
||||||
|
}}
|
||||||
|
className={styles.fullWidth}
|
||||||
|
>
|
||||||
|
<BookingFlowInput
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.emailAddress",
|
||||||
|
defaultMessage: "Email address",
|
||||||
|
})}
|
||||||
|
name="email"
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.phoneNumber = el
|
||||||
|
}}
|
||||||
|
className={styles.fullWidth}
|
||||||
|
>
|
||||||
|
<Phone
|
||||||
|
countryLabel={intl.formatMessage({
|
||||||
|
id: "common.countryCode",
|
||||||
|
defaultMessage: "Country code",
|
||||||
|
})}
|
||||||
|
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||||
|
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||||
|
errorMessage={getErrorMessage(
|
||||||
|
intl,
|
||||||
|
config.variant,
|
||||||
|
errors.phoneNumber?.message
|
||||||
|
)}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.phoneNumber",
|
||||||
|
defaultMessage: "Phone number",
|
||||||
|
})}
|
||||||
|
name="phoneNumber"
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showMembershipIdInput ? (
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.membershipNo = el
|
||||||
|
}}
|
||||||
|
className={styles.fullWidth}
|
||||||
|
>
|
||||||
|
<BookingFlowInput
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.membershipId",
|
||||||
|
defaultMessage: "Membership ID",
|
||||||
|
})}
|
||||||
|
name="membershipNo"
|
||||||
|
type="tel"
|
||||||
|
registerOptions={{ onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
|
<SpecialRequests registerOptions={{ onBlur: updateDetailsStore }} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ export default function Signup({
|
|||||||
errors,
|
errors,
|
||||||
name,
|
name,
|
||||||
registerOptions,
|
registerOptions,
|
||||||
|
refs,
|
||||||
}: {
|
}: {
|
||||||
errors: FieldErrors
|
errors: FieldErrors
|
||||||
name: string
|
name: string
|
||||||
registerOptions?: RegisterOptions
|
registerOptions?: RegisterOptions
|
||||||
|
refs: React.RefObject<Record<string, HTMLElement | null>>
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -45,15 +47,26 @@ export default function Signup({
|
|||||||
if (isJoinChecked)
|
if (isJoinChecked)
|
||||||
return (
|
return (
|
||||||
<div className={styles.additionalFormData}>
|
<div className={styles.additionalFormData}>
|
||||||
<BookingFlowInput
|
<div
|
||||||
name="zipCode"
|
ref={(el) => {
|
||||||
label={intl.formatMessage({
|
refs.current.zipCode = el
|
||||||
id: "common.zipCode",
|
}}
|
||||||
defaultMessage: "Zip code",
|
>
|
||||||
})}
|
<BookingFlowInput
|
||||||
registerOptions={{ required: true, ...registerOptions }}
|
name="zipCode"
|
||||||
/>
|
label={intl.formatMessage({
|
||||||
<div className={styles.dateField}>
|
id: "common.zipCode",
|
||||||
|
defaultMessage: "Zip code",
|
||||||
|
})}
|
||||||
|
registerOptions={{ required: true, ...registerOptions }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.dateField}
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.dateOfBirth = el
|
||||||
|
}}
|
||||||
|
>
|
||||||
<header>
|
<header>
|
||||||
<Caption type="bold">
|
<Caption type="bold">
|
||||||
<span className={styles.required}>
|
<span className={styles.required}>
|
||||||
@@ -94,5 +107,13 @@ export default function Signup({
|
|||||||
|
|
||||||
if (config.enterDetailsMembershipIdInputLocation === "join-card") return null
|
if (config.enterDetailsMembershipIdInputLocation === "join-card") return null
|
||||||
|
|
||||||
return <MembershipNumberInput registerOptions={registerOptions} />
|
return (
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.membershipNo = el
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MembershipNumberInput registerOptions={registerOptions} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useCallback, useEffect } from "react"
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ type DetailsProps = {
|
|||||||
|
|
||||||
const formID = "enter-details"
|
const formID = "enter-details"
|
||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user }: DetailsProps) {
|
||||||
|
const refs = useRef<Record<string, HTMLElement | null>>({})
|
||||||
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const config = useBookingFlowConfig()
|
const config = useBookingFlowConfig()
|
||||||
@@ -107,12 +109,36 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function callback() {
|
async function callback() {
|
||||||
trigger()
|
await trigger()
|
||||||
trackFormSubmit()
|
trackFormSubmit()
|
||||||
|
const baseFieldOrder = [
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
"countryCode",
|
||||||
|
"email",
|
||||||
|
"phoneNumber",
|
||||||
|
"membershipNo",
|
||||||
|
]
|
||||||
|
const joinChecked = methods.watch("join")
|
||||||
|
const fieldOrder = joinChecked
|
||||||
|
? [...baseFieldOrder, "zipCode", "dateOfBirth"]
|
||||||
|
: baseFieldOrder
|
||||||
|
for (const name of fieldOrder) {
|
||||||
|
const fieldError =
|
||||||
|
methods.formState.errors[
|
||||||
|
name as keyof typeof methods.formState.errors
|
||||||
|
]
|
||||||
|
if (fieldError && refs.current[name]) {
|
||||||
|
return refs.current[name] ?? undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addPreSubmitCallback(`${idx}-details`, callback)
|
addPreSubmitCallback(`${idx}-details`, callback)
|
||||||
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit])
|
}, [addPreSubmitCallback, idx, trigger, trackFormSubmit, methods])
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: GuestDetailsSchema) => {
|
(values: GuestDetailsSchema) => {
|
||||||
@@ -133,12 +159,12 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
setIncomplete()
|
setIncomplete()
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
handleSubmit,
|
|
||||||
formState.isValid,
|
formState.isValid,
|
||||||
|
handleSubmit,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
setIncomplete,
|
|
||||||
updatePartialGuestData,
|
updatePartialGuestData,
|
||||||
getValues,
|
getValues,
|
||||||
|
setIncomplete,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(updateDetailsStore, [updateDetailsStore])
|
useEffect(updateDetailsStore, [updateDetailsStore])
|
||||||
@@ -174,83 +200,114 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
defaultMessage: "Guest information",
|
defaultMessage: "Guest information",
|
||||||
})}
|
})}
|
||||||
</Footnote>
|
</Footnote>
|
||||||
<BookingFlowInput
|
<div
|
||||||
autoComplete="given-name"
|
ref={(el) => {
|
||||||
label={intl.formatMessage({
|
refs.current.firstName = el
|
||||||
id: "common.firstName",
|
}}
|
||||||
defaultMessage: "First name",
|
>
|
||||||
})}
|
<BookingFlowInput
|
||||||
maxLength={30}
|
autoComplete="given-name"
|
||||||
name="firstName"
|
label={intl.formatMessage({
|
||||||
readOnly={!!user}
|
id: "common.firstName",
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
defaultMessage: "First name",
|
||||||
/>
|
})}
|
||||||
<BookingFlowInput
|
maxLength={30}
|
||||||
autoComplete="family-name"
|
name="firstName"
|
||||||
label={intl.formatMessage({
|
readOnly={!!user}
|
||||||
id: "common.lastName",
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
defaultMessage: "Last name",
|
/>
|
||||||
})}
|
</div>
|
||||||
maxLength={30}
|
<div
|
||||||
name="lastName"
|
ref={(el) => {
|
||||||
readOnly={!!user}
|
refs.current.lastName = el
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
}}
|
||||||
/>
|
>
|
||||||
<CountrySelect
|
<BookingFlowInput
|
||||||
|
autoComplete="family-name"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "common.lastName",
|
||||||
|
defaultMessage: "Last name",
|
||||||
|
})}
|
||||||
|
maxLength={30}
|
||||||
|
name="lastName"
|
||||||
|
readOnly={!!user}
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.countryCode = el
|
||||||
|
}}
|
||||||
className={styles.fullWidth}
|
className={styles.fullWidth}
|
||||||
label={intl.formatMessage({
|
>
|
||||||
id: "common.country",
|
<CountrySelect
|
||||||
defaultMessage: "Country",
|
label={intl.formatMessage({
|
||||||
})}
|
id: "common.country",
|
||||||
lang={lang}
|
defaultMessage: "Country",
|
||||||
countries={getFormattedCountryList(intl)}
|
})}
|
||||||
errorMessage={getErrorMessage(
|
lang={lang}
|
||||||
intl,
|
countries={getFormattedCountryList(intl)}
|
||||||
config.variant,
|
errorMessage={getErrorMessage(
|
||||||
formState.errors.countryCode?.message
|
intl,
|
||||||
)}
|
config.variant,
|
||||||
name="countryCode"
|
formState.errors.countryCode?.message
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
)}
|
||||||
disabled={!!user}
|
name="countryCode"
|
||||||
/>
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
<BookingFlowInput
|
disabled={!!user}
|
||||||
autoComplete="email"
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.email = el
|
||||||
|
}}
|
||||||
className={styles.fullWidth}
|
className={styles.fullWidth}
|
||||||
label={intl.formatMessage({
|
>
|
||||||
id: "common.emailAddress",
|
<BookingFlowInput
|
||||||
defaultMessage: "Email address",
|
autoComplete="email"
|
||||||
})}
|
label={intl.formatMessage({
|
||||||
name="email"
|
id: "common.emailAddress",
|
||||||
readOnly={!!user}
|
defaultMessage: "Email address",
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
})}
|
||||||
/>
|
name="email"
|
||||||
<Phone
|
readOnly={!!user}
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
refs.current.phoneNumber = el
|
||||||
|
}}
|
||||||
className={styles.fullWidth}
|
className={styles.fullWidth}
|
||||||
countryLabel={intl.formatMessage({
|
>
|
||||||
id: "common.countryCode",
|
<Phone
|
||||||
defaultMessage: "Country code",
|
countryLabel={intl.formatMessage({
|
||||||
})}
|
id: "common.countryCode",
|
||||||
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
defaultMessage: "Country code",
|
||||||
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
})}
|
||||||
errorMessage={getErrorMessage(
|
countriesWithTranslatedName={getFormattedCountryList(intl)}
|
||||||
intl,
|
defaultCountryCode={getDefaultCountryFromLang(lang)}
|
||||||
config.variant,
|
errorMessage={getErrorMessage(
|
||||||
formState.errors.phoneNumber?.message
|
intl,
|
||||||
)}
|
config.variant,
|
||||||
label={intl.formatMessage({
|
formState.errors.phoneNumber?.message
|
||||||
id: "common.phoneNumber",
|
)}
|
||||||
defaultMessage: "Phone number",
|
label={intl.formatMessage({
|
||||||
})}
|
id: "common.phoneNumber",
|
||||||
name="phoneNumber"
|
defaultMessage: "Phone number",
|
||||||
disabled={!!user}
|
})}
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
name="phoneNumber"
|
||||||
/>
|
disabled={!!user}
|
||||||
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{user ? null : (
|
{user ? null : (
|
||||||
<div className={styles.fullWidth}>
|
<div className={styles.fullWidth}>
|
||||||
<Signup
|
<Signup
|
||||||
errors={formState.errors}
|
errors={formState.errors}
|
||||||
name="join"
|
name="join"
|
||||||
registerOptions={{ onBlur: updateDetailsStore }}
|
registerOptions={{ onBlur: updateDetailsStore }}
|
||||||
|
refs={refs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -95,15 +95,15 @@ export default function PaymentClient({
|
|||||||
rooms,
|
rooms,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
preSubmitCallbacks,
|
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
|
runPreSubmitCallbacks,
|
||||||
} = useEnterDetailsStore((state) => ({
|
} = useEnterDetailsStore((state) => ({
|
||||||
booking: state.booking,
|
booking: state.booking,
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
totalPrice: state.totalPrice,
|
totalPrice: state.totalPrice,
|
||||||
preSubmitCallbacks: state.preSubmitCallbacks,
|
|
||||||
isSubmitting: state.isSubmitting,
|
isSubmitting: state.isSubmitting,
|
||||||
setIsSubmitting: state.actions.setIsSubmitting,
|
setIsSubmitting: state.actions.setIsSubmitting,
|
||||||
|
runPreSubmitCallbacks: state.actions.runPreSubmitCallbacks,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
||||||
@@ -312,35 +312,39 @@ export default function PaymentClient({
|
|||||||
[hasFlexRates]
|
[hasFlexRates]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const scrollToInvalidField = useCallback(async (): Promise<boolean> => {
|
||||||
|
// If any room is not complete/valid, scroll to the first invalid field, this is needed as rooms and other fields are in separate forms
|
||||||
|
|
||||||
|
const invalidField = await runPreSubmitCallbacks()
|
||||||
|
const errorNames = Object.keys(methods.formState.errors)
|
||||||
|
const firstIncompleteRoomIndex = rooms.findIndex((room) => !room.isComplete)
|
||||||
|
|
||||||
|
const scrollToElement = (el: HTMLElement) => {
|
||||||
|
const offset = getTopOffset()
|
||||||
|
const top = el.getBoundingClientRect().top + window.scrollY - offset - 20
|
||||||
|
window.scrollTo({ top, behavior: "smooth" })
|
||||||
|
const input = el.querySelector<HTMLElement>("input")
|
||||||
|
input?.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidField) {
|
||||||
|
scrollToElement(invalidField)
|
||||||
|
} else if (errorNames.length > 0) {
|
||||||
|
const firstErrorEl = document.querySelector(`[name="${errorNames[0]}"]`)
|
||||||
|
if (firstErrorEl) {
|
||||||
|
scrollToElement(firstErrorEl as HTMLElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstIncompleteRoomIndex !== -1
|
||||||
|
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(data: PaymentFormData) => {
|
async (data: PaymentFormData) => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
Object.values(preSubmitCallbacks).forEach((callback) => {
|
const isRoomInvalid = await scrollToInvalidField()
|
||||||
callback()
|
if (isRoomInvalid) {
|
||||||
})
|
|
||||||
const firstIncompleteRoomIndex = rooms.findIndex(
|
|
||||||
(room) => !room.isComplete
|
|
||||||
)
|
|
||||||
|
|
||||||
// If any room is not complete/valid, scroll to it
|
|
||||||
if (firstIncompleteRoomIndex !== -1) {
|
|
||||||
const roomElement = document.getElementById(
|
|
||||||
`room-${firstIncompleteRoomIndex + 1}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!roomElement) {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const roomElementTop =
|
|
||||||
roomElement.getBoundingClientRect().top + window.scrollY
|
|
||||||
|
|
||||||
window.scrollTo({
|
|
||||||
top: roomElementTop - getTopOffset() - 20,
|
|
||||||
behavior: "smooth",
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -502,13 +506,11 @@ export default function PaymentClient({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
initiateBooking.mutate(payload)
|
initiateBooking.mutate(payload)
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
preSubmitCallbacks,
|
scrollToInvalidField,
|
||||||
rooms,
|
|
||||||
getPaymentMethod,
|
getPaymentMethod,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
lang,
|
lang,
|
||||||
@@ -517,8 +519,8 @@ export default function PaymentClient({
|
|||||||
fromDate,
|
fromDate,
|
||||||
toDate,
|
toDate,
|
||||||
hotelId,
|
hotelId,
|
||||||
|
rooms,
|
||||||
initiateBooking,
|
initiateBooking,
|
||||||
getTopOffset,
|
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
booking.rooms,
|
booking.rooms,
|
||||||
user?.data?.partnerLoyaltyNumber,
|
user?.data?.partnerLoyaltyNumber,
|
||||||
@@ -534,6 +536,13 @@ export default function PaymentClient({
|
|||||||
defaultMessage: "Select payment method",
|
defaultMessage: "Select payment method",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleInvalidSubmit = async () => {
|
||||||
|
const valid = await methods.trigger()
|
||||||
|
if (!valid) {
|
||||||
|
await scrollToInvalidField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cx(styles.paymentSection, {
|
className={cx(styles.paymentSection, {
|
||||||
@@ -549,7 +558,7 @@ export default function PaymentClient({
|
|||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
className={styles.paymentContainer}
|
className={styles.paymentContainer}
|
||||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
onSubmit={methods.handleSubmit(handleSubmit, handleInvalidSubmit)}
|
||||||
id={formId}
|
id={formId}
|
||||||
>
|
>
|
||||||
{booking.searchType === SEARCH_TYPE_REDEMPTION ? (
|
{booking.searchType === SEARCH_TYPE_REDEMPTION ? (
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export function RoomSidePeekContent({ room }: RoomSidePeekContentProps) {
|
|||||||
<ul className={styles.facilityList}>
|
<ul className={styles.facilityList}>
|
||||||
{[...room.roomFacilities]
|
{[...room.roomFacilities]
|
||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
.map((facility) => {
|
.map((facility, index) => {
|
||||||
return (
|
return (
|
||||||
<li key={facility.name}>
|
<li key={`${facility.name}-${index}`}>
|
||||||
<FacilityIcon
|
<FacilityIcon
|
||||||
name={facility.icon}
|
name={facility.icon}
|
||||||
size={24}
|
size={24}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function createDetailsStore(
|
|||||||
return total
|
return total
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
return create<DetailsState>()((set) => ({
|
return create<DetailsState>()((set, get) => ({
|
||||||
availableBeds,
|
availableBeds,
|
||||||
booking: initialState.booking,
|
booking: initialState.booking,
|
||||||
roomCategories: initialState.roomCategories,
|
roomCategories: initialState.roomCategories,
|
||||||
@@ -404,6 +404,47 @@ export function createDetailsStore(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async runPreSubmitCallbacks(): Promise<HTMLElement | undefined> {
|
||||||
|
const callbacks = get().preSubmitCallbacks
|
||||||
|
const stepOrder = ["bedType", "breakfast", "details"]
|
||||||
|
|
||||||
|
const sortedKeys = Object.keys(callbacks).sort((a, b) => {
|
||||||
|
const [aIdx, aStep] = a.split("-")
|
||||||
|
const [bIdx, bStep] = b.split("-")
|
||||||
|
if (aIdx !== bIdx) return Number(aIdx) - Number(bIdx)
|
||||||
|
return stepOrder.indexOf(aStep) - stepOrder.indexOf(bStep)
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomsMap = new Map<string, string[]>()
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
const [roomIdx] = key.split("-")
|
||||||
|
if (!roomsMap.has(roomIdx)) {
|
||||||
|
roomsMap.set(roomIdx, [])
|
||||||
|
}
|
||||||
|
roomsMap.get(roomIdx)?.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstInvalidElement: HTMLElement | undefined = undefined
|
||||||
|
|
||||||
|
for (const roomIdx of Array.from(roomsMap.keys()).sort(
|
||||||
|
(a, b) => Number(a) - Number(b)
|
||||||
|
)) {
|
||||||
|
const roomKeys = roomsMap.get(roomIdx)!
|
||||||
|
const invalidElementsInRoom: HTMLElement[] = []
|
||||||
|
|
||||||
|
for (const key of roomKeys) {
|
||||||
|
const el = await callbacks[key]()
|
||||||
|
if (el) invalidElementsInRoom.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstInvalidElement && invalidElementsInRoom.length > 0) {
|
||||||
|
firstInvalidElement = invalidElementsInRoom.at(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstInvalidElement
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ export interface DetailsState {
|
|||||||
setIsSubmitting: (isSubmitting: boolean) => void
|
setIsSubmitting: (isSubmitting: boolean) => void
|
||||||
toggleSummaryOpen: () => void
|
toggleSummaryOpen: () => void
|
||||||
updateSeachParamString: (searchParamString: string) => void
|
updateSeachParamString: (searchParamString: string) => void
|
||||||
addPreSubmitCallback: (name: string, callback: () => void) => void
|
addPreSubmitCallback: (
|
||||||
|
name: string,
|
||||||
|
callback: () => Promise<HTMLElement | undefined>
|
||||||
|
) => void
|
||||||
|
runPreSubmitCallbacks: () => Promise<HTMLElement | undefined>
|
||||||
}
|
}
|
||||||
availableBeds: Record<string, number>
|
availableBeds: Record<string, number>
|
||||||
booking: DetailsBooking
|
booking: DetailsBooking
|
||||||
@@ -112,7 +116,7 @@ export interface DetailsState {
|
|||||||
hotelName: string
|
hotelName: string
|
||||||
roomCategories: RoomCategories
|
roomCategories: RoomCategories
|
||||||
defaultCurrency: CurrencyEnum
|
defaultCurrency: CurrencyEnum
|
||||||
preSubmitCallbacks: Record<string, () => void>
|
preSubmitCallbacks: Record<string, () => Promise<HTMLElement | undefined>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PersistedState = {
|
export type PersistedState = {
|
||||||
|
|||||||
@@ -38,11 +38,3 @@
|
|||||||
.topAlign {
|
.topAlign {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
|
||||||
align-items: center;
|
|
||||||
color: var(--Scandic-Red-60);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x-half);
|
|
||||||
margin: var(--Spacing-x1) 0 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
|
|
||||||
import styles from './checkbox.module.css'
|
import styles from './checkbox.module.css'
|
||||||
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
import { MaterialIcon } from '../../Icons/MaterialIcon'
|
||||||
import Caption from '../../Caption'
|
import { ErrorMessage } from '../ErrorMessage'
|
||||||
|
|
||||||
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
name: string
|
name: string
|
||||||
@@ -36,7 +36,7 @@ const Checkbox = forwardRef<
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const { control } = useFormContext()
|
const { control } = useFormContext()
|
||||||
const { field, fieldState } = useController({
|
const { field, fieldState, formState } = useController({
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
rules: registerOptions,
|
rules: registerOptions,
|
||||||
@@ -48,6 +48,7 @@ const Checkbox = forwardRef<
|
|||||||
isSelected={field.value}
|
isSelected={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
|
name={name}
|
||||||
isDisabled={registerOptions?.disabled}
|
isDisabled={registerOptions?.disabled}
|
||||||
excludeFromTabOrder
|
excludeFromTabOrder
|
||||||
>
|
>
|
||||||
@@ -68,12 +69,15 @@ const Checkbox = forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
{fieldState.error && !hideError ? (
|
{fieldState.error && !hideError ? (
|
||||||
<Caption className={styles.error} fontOnly>
|
<ErrorMessage
|
||||||
<MaterialIcon icon="info" color="Icon/Interactive/Accent" />
|
errors={formState.errors}
|
||||||
{(fieldState.error.message &&
|
name={name}
|
||||||
errorCodeMessages?.[fieldState.error.message]) ||
|
messageLabel={
|
||||||
fieldState.error.message}
|
(fieldState.error.message &&
|
||||||
</Caption>
|
errorCodeMessages?.[fieldState.error.message]) ||
|
||||||
|
fieldState.error.message
|
||||||
|
}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
&[data-invalid] {
|
&[data-invalid] {
|
||||||
border-color: var(--Border-Interactive-Error);
|
border-color: var(--Border-Interactive-Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-invalid][data-focused] {
|
||||||
|
outline: 2px solid var(--Border-Interactive-Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ export function Error({ children }: React.PropsWithChildren) {
|
|||||||
variant="Body/Supporting text (caption)/smRegular"
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<MaterialIcon icon="info" color="Icon/Feedback/Error" />
|
<MaterialIcon
|
||||||
|
icon="error"
|
||||||
|
color="Icon/Feedback/Error"
|
||||||
|
isFilled
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||||
|
import { MessageBanner } from './index'
|
||||||
|
|
||||||
|
type MessageBannerType = 'default' | 'error' | 'info'
|
||||||
|
type TextColor = 'default' | 'error'
|
||||||
|
|
||||||
|
const meta: Meta<typeof MessageBanner> = {
|
||||||
|
title: 'Components/MessageBanner',
|
||||||
|
component: MessageBanner,
|
||||||
|
argTypes: {
|
||||||
|
type: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['default', 'error', 'info'] as MessageBannerType[],
|
||||||
|
},
|
||||||
|
textColor: {
|
||||||
|
control: { type: 'select' },
|
||||||
|
options: ['default', 'error'] as TextColor[],
|
||||||
|
},
|
||||||
|
text: { control: 'text' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof MessageBanner>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'default',
|
||||||
|
textColor: 'default',
|
||||||
|
text: 'This is a default message',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Warning: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'error',
|
||||||
|
textColor: 'default',
|
||||||
|
text: 'This is a warning message',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WarningErrorText: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'error',
|
||||||
|
textColor: 'error',
|
||||||
|
text: 'Warning with error text color',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Info: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'info',
|
||||||
|
textColor: 'default',
|
||||||
|
text: 'This is an info message',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfoErrorText: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'info',
|
||||||
|
textColor: 'error',
|
||||||
|
text: 'Info with error text color',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import styles from './messageBanner.module.css'
|
||||||
|
import { Typography } from '../Typography'
|
||||||
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
|
|
||||||
|
type MessageBannerType = 'default' | 'error' | 'info'
|
||||||
|
type TextColor = 'default' | 'error'
|
||||||
|
|
||||||
|
const textVariants = cva('', {
|
||||||
|
variants: {
|
||||||
|
textColor: {
|
||||||
|
default: styles.textDefault,
|
||||||
|
error: styles.textError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
textColor: 'default',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type MessageBannerProps = {
|
||||||
|
type?: MessageBannerType
|
||||||
|
textColor?: TextColor
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageBanner({
|
||||||
|
type = 'default',
|
||||||
|
textColor = 'default',
|
||||||
|
text,
|
||||||
|
}: MessageBannerProps) {
|
||||||
|
const textClass = textVariants({ textColor })
|
||||||
|
|
||||||
|
const iconName = type === 'error' ? 'error' : 'info'
|
||||||
|
const iconColor =
|
||||||
|
type === 'error'
|
||||||
|
? 'Icon/Feedback/Error'
|
||||||
|
: type === 'info'
|
||||||
|
? 'Icon/Feedback/Information'
|
||||||
|
: 'Icon/Default'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Typography
|
||||||
|
className={textClass}
|
||||||
|
variant="Body/Supporting text (caption)/smRegular"
|
||||||
|
>
|
||||||
|
<span className={styles.content}>
|
||||||
|
<MaterialIcon size={20} icon={iconName} color={iconColor} isFilled />
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
background-color: var(--Surface-Primary-Default);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
border: 1px solid var(--Border-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textDefault {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textError {
|
||||||
|
color: var(--Text-Feedback-Error-Accent);
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ export const DinersClubIcon = (props: PaymentIconProps) => (
|
|||||||
y2="21.6"
|
y2="21.6"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#3479C0" />
|
<stop stopColor="#3479C0" />
|
||||||
<stop offset="1" stop-color="#133362" />
|
<stop offset="1" stopColor="#133362" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="clip0_5382_46858">
|
<clipPath id="clip0_5382_46858">
|
||||||
<rect width="48" height="32" fill="white" />
|
<rect width="48" height="32" fill="white" />
|
||||||
|
|||||||
@@ -89,10 +89,10 @@ export const DiscoverIcon = (props: PaymentIconProps) => (
|
|||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
gradientTransform="translate(28.6 17.5998) rotate(-142.431) scale(6.56048 6.47264)"
|
gradientTransform="translate(28.6 17.5998) rotate(-142.431) scale(6.56048 6.47264)"
|
||||||
>
|
>
|
||||||
<stop stop-color="#F59900" />
|
<stop stopColor="#F59900" />
|
||||||
<stop offset="0.210082" stop-color="#F39501" />
|
<stop offset="0.210082" stopColor="#F39501" />
|
||||||
<stop offset="0.908163" stop-color="#CE3C0B" />
|
<stop offset="0.908163" stopColor="#CE3C0B" />
|
||||||
<stop offset="1" stop-color="#A4420A" />
|
<stop offset="1" stopColor="#A4420A" />
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
<clipPath id="clip0_5382_46865">
|
<clipPath id="clip0_5382_46865">
|
||||||
<rect width="48" height="32" rx="3" fill="white" />
|
<rect width="48" height="32" rx="3" fill="white" />
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const JcbIcon = (props: PaymentIconProps) => (
|
|||||||
y2="16.0328"
|
y2="16.0328"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#007940" />
|
<stop stopColor="#007940" />
|
||||||
<stop offset="0.2285" stop-color="#00873F" />
|
<stop offset="0.2285" stopColor="#00873F" />
|
||||||
<stop offset="0.7433" stop-color="#40A737" />
|
<stop offset="0.7433" stopColor="#40A737" />
|
||||||
<stop offset="1" stop-color="#5CB531" />
|
<stop offset="1" stopColor="#5CB531" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_5382_46863"
|
id="paint1_linear_5382_46863"
|
||||||
@@ -62,10 +62,10 @@ export const JcbIcon = (props: PaymentIconProps) => (
|
|||||||
y2="16.001"
|
y2="16.001"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#007940" />
|
<stop stopColor="#007940" />
|
||||||
<stop offset="0.2285" stop-color="#00873F" />
|
<stop offset="0.2285" stopColor="#00873F" />
|
||||||
<stop offset="0.7433" stop-color="#40A737" />
|
<stop offset="0.7433" stopColor="#40A737" />
|
||||||
<stop offset="1" stop-color="#5CB531" />
|
<stop offset="1" stopColor="#5CB531" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint2_linear_5382_46863"
|
id="paint2_linear_5382_46863"
|
||||||
@@ -75,10 +75,10 @@ export const JcbIcon = (props: PaymentIconProps) => (
|
|||||||
y2="14.4771"
|
y2="14.4771"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#007940" />
|
<stop stopColor="#007940" />
|
||||||
<stop offset="0.2285" stop-color="#00873F" />
|
<stop offset="0.2285" stopColor="#00873F" />
|
||||||
<stop offset="0.7433" stop-color="#40A737" />
|
<stop offset="0.7433" stopColor="#40A737" />
|
||||||
<stop offset="1" stop-color="#5CB531" />
|
<stop offset="1" stopColor="#5CB531" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint3_linear_5382_46863"
|
id="paint3_linear_5382_46863"
|
||||||
@@ -88,11 +88,11 @@ export const JcbIcon = (props: PaymentIconProps) => (
|
|||||||
y2="16.001"
|
y2="16.001"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#6C2C2F" />
|
<stop stopColor="#6C2C2F" />
|
||||||
<stop offset="0.1735" stop-color="#882730" />
|
<stop offset="0.1735" stopColor="#882730" />
|
||||||
<stop offset="0.5731" stop-color="#BE1833" />
|
<stop offset="0.5731" stopColor="#BE1833" />
|
||||||
<stop offset="0.8585" stop-color="#DC0436" />
|
<stop offset="0.8585" stopColor="#DC0436" />
|
||||||
<stop offset="1" stop-color="#E60039" />
|
<stop offset="1" stopColor="#E60039" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint4_linear_5382_46863"
|
id="paint4_linear_5382_46863"
|
||||||
@@ -102,10 +102,10 @@ export const JcbIcon = (props: PaymentIconProps) => (
|
|||||||
y2="16.001"
|
y2="16.001"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#1F286F" />
|
<stop stopColor="#1F286F" />
|
||||||
<stop offset="0.4751" stop-color="#004E94" />
|
<stop offset="0.4751" stopColor="#004E94" />
|
||||||
<stop offset="0.8261" stop-color="#0066B1" />
|
<stop offset="0.8261" stopColor="#0066B1" />
|
||||||
<stop offset="1" stop-color="#006FBC" />
|
<stop offset="1" stopColor="#006FBC" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="clip0_5382_46863">
|
<clipPath id="clip0_5382_46863">
|
||||||
<rect width="48" height="32" rx="3" fill="white" />
|
<rect width="48" height="32" rx="3" fill="white" />
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export const SwishIcon = (props: PaymentIconProps) => {
|
|||||||
y2="13.9987"
|
y2="13.9987"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#EF3220" />
|
<stop stopColor="#EF3220" />
|
||||||
<stop offset="1" stop-color="#FCD205" />
|
<stop offset="1" stopColor="#FCD205" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_5382_46851"
|
id="paint1_linear_5382_46851"
|
||||||
@@ -72,10 +72,10 @@ export const SwishIcon = (props: PaymentIconProps) => {
|
|||||||
y2="21.8844"
|
y2="21.8844"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#FCD205" />
|
<stop stopColor="#FCD205" />
|
||||||
<stop offset="0.263921" stop-color="#F47216" />
|
<stop offset="0.263921" stopColor="#F47216" />
|
||||||
<stop offset="0.560797" stop-color="#B31A93" />
|
<stop offset="0.560797" stopColor="#B31A93" />
|
||||||
<stop offset="1" stop-color="#2743A0" />
|
<stop offset="1" stopColor="#2743A0" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint2_linear_5382_46851"
|
id="paint2_linear_5382_46851"
|
||||||
@@ -85,10 +85,10 @@ export const SwishIcon = (props: PaymentIconProps) => {
|
|||||||
y2="18.0191"
|
y2="18.0191"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#7FD3B9" />
|
<stop stopColor="#7FD3B9" />
|
||||||
<stop offset="0.265705" stop-color="#66CDE1" />
|
<stop offset="0.265705" stopColor="#66CDE1" />
|
||||||
<stop offset="0.554471" stop-color="#6D8ED1" />
|
<stop offset="0.554471" stopColor="#6D8ED1" />
|
||||||
<stop offset="1" stop-color="#2743A0" />
|
<stop offset="1" stopColor="#2743A0" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint3_linear_5382_46851"
|
id="paint3_linear_5382_46851"
|
||||||
@@ -98,10 +98,10 @@ export const SwishIcon = (props: PaymentIconProps) => {
|
|||||||
y2="10.1074"
|
y2="10.1074"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stop-color="#1E5CB2" />
|
<stop stopColor="#1E5CB2" />
|
||||||
<stop offset="0.246658" stop-color="#4DC4CE" />
|
<stop offset="0.246658" stopColor="#4DC4CE" />
|
||||||
<stop offset="0.564821" stop-color="#66C657" />
|
<stop offset="0.564821" stopColor="#66C657" />
|
||||||
<stop offset="1" stop-color="#FCD205" />
|
<stop offset="1" stopColor="#FCD205" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="clip0_5382_46851">
|
<clipPath id="clip0_5382_46851">
|
||||||
<rect width="48" height="32" fill="white" />
|
<rect width="48" height="32" fill="white" />
|
||||||
|
|||||||
@@ -52,6 +52,9 @@
|
|||||||
&[data-invalid] {
|
&[data-invalid] {
|
||||||
border-color: var(--Border-Interactive-Error);
|
border-color: var(--Border-Interactive-Error);
|
||||||
}
|
}
|
||||||
|
&[data-invalid][data-focused] {
|
||||||
|
outline: 2px solid var(--Border-Interactive-Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
"./Map/Markers/HotelMarkerByType": "./lib/components/Map/Markers/HotelMarkerByType.tsx",
|
"./Map/Markers/HotelMarkerByType": "./lib/components/Map/Markers/HotelMarkerByType.tsx",
|
||||||
"./Map/Markers/PoiMarker": "./lib/components/Map/Markers/PoiMarker/index.tsx",
|
"./Map/Markers/PoiMarker": "./lib/components/Map/Markers/PoiMarker/index.tsx",
|
||||||
"./Map/types": "./lib/components/Map/types.ts",
|
"./Map/types": "./lib/components/Map/types.ts",
|
||||||
|
"./MessageBanner": "./lib/components/MessageBanner/index.tsx",
|
||||||
"./Modal": "./lib/components/Modal/index.tsx",
|
"./Modal": "./lib/components/Modal/index.tsx",
|
||||||
"./Modal/ModalContentWithActions": "./lib/components/Modal/ModalContentWithActions/index.tsx",
|
"./Modal/ModalContentWithActions": "./lib/components/Modal/ModalContentWithActions/index.tsx",
|
||||||
"./NoRateAvailableCard": "./lib/components/RateCard/NoRateAvailable/index.tsx",
|
"./NoRateAvailableCard": "./lib/components/RateCard/NoRateAvailable/index.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user